Для чего используются линзы?

Кажется, я не могу найти объяснения того, для чего используются линзы в практических примерах. Этот короткий абзац со страницы Hackage - самый близкий, который я нашел:

Эти модули обеспечивают удобный способ доступа к элементам конструкции и их обновления. Он очень похож на Data.Accessors, но немного более общий и имеет меньше зависимостей. Мне особенно нравится, насколько чисто он обрабатывает вложенные структуры в монадах состояний.

Итак, для чего они используются? Какие преимущества и недостатки они имеют по сравнению с другими методами? Зачем они нужны?


person Pubby    schedule 28.05.2012    source источник
comment
Возможно, вам понравится лекция Эдварда Кметта Линзы: функциональный императив. Он представлен на Scala, но перевод на полезность линз в Haskell должен быть ясен.   -  person Dan Burton    schedule 29.05.2012


Ответы (3)


Они предлагают чистую абстракцию по поводу обновлений данных и никогда не «нужны». Они просто позволяют вам рассуждать о проблеме по-другому.

В некоторых императивных / «объектно-ориентированных» языках программирования, таких как C, у вас есть знакомая концепция некоторого набора значений (назовем их «структурами») и способы пометить каждое значение в коллекции (метки обычно называются «полями»). ). Это приводит к следующему определению:

typedef struct { /* defining a new struct type */
  float x; /* field */
  float y; /* field */
} Vec2;

typedef struct {
  Vec2 col1; /* nested structs */
  Vec2 col2;
} Mat2;

Затем вы можете создать значения этого нового определенного типа следующим образом:

Vec2 vec = { 2.0f, 3.0f };
/* Reading the components of vec */
float foo = vec.x;
/* Writing to the components of vec */
vec.y = foo;

Mat2 mat = { vec, vec };
/* Changing a nested field in the matrix */
mat.col2.x = 4.0f;

Аналогично в Haskell у нас есть типы данных:

data Vec2 =
  Vec2
  { vecX :: Float
  , vecY :: Float
  }

data Mat2 =
  Mat2
  { matCol1 :: Vec2
  , matCol2 :: Vec2
  }

Затем этот тип данных используется следующим образом:

let vec  = Vec2 2 3
    -- Reading the components of vec
    foo  = vecX vec
    -- Creating a new vector with some component changed.
    vec2 = vec { vecY = foo }

    mat = Mat2 vec2 vec2

Однако в Haskell нет простого способа изменить вложенные поля в структуре данных. Это связано с тем, что вам нужно воссоздать все объекты-оболочки вокруг изменяемого значения, потому что значения Haskell неизменяемы. Если у вас есть матрица, подобная приведенной выше, в Haskell, и вы хотите изменить правую верхнюю ячейку в матрице, вы должны написать это:

    mat2 = mat { matCol2 = (matCol2 mat) { vecX = 4 } }

Работает, но выглядит коряво. Итак, то, что кто-то придумал, в основном заключается в следующем: если вы сгруппируете две вещи вместе: "получатель" значения (например, vecX и matCol2 выше) с соответствующей функцией, которая, учитывая структуру данных, к которой принадлежит получатель, может создать новую структуру данных с измененным значением, вы сможете делать много полезных вещей. Например:

data Data = Data { member :: Int }

-- The "getter" of the member variable
getMember :: Data -> Int
getMember d = member d

-- The "setter" or more accurately "updater" of the member variable
setMember :: Data -> Int -> Data
setMember d m = d { member = m }

memberLens :: (Data -> Int, Data -> Int -> Data)
memberLens = (getMember, setMember)

Есть много способов применения линз; для этого текста предположим, что линза похожа на приведенную выше:

type Lens a b = (a -> b, a -> b -> a)

Т.е. это комбинация получателя и установщика для некоторого типа a, который имеет поле типа b, поэтому memberLens выше будет Lens Data Int. Что это позволяет нам делать?

Что ж, давайте сначала создадим две простые функции, извлекающие геттеры и сеттеры из линзы:

getL :: Lens a b -> a -> b
getL (getter, setter) = getter

setL :: Lens a b -> a -> b -> a
setL (getter, setter) = setter

Теперь мы можем начать абстрагироваться от всего. Давайте снова возьмем ситуацию, описанную выше, когда мы хотим изменить значение «на два этажа». Добавляем структуру данных с другой линзой:

data Foo = Foo { subData :: Data }

subDataLens :: Lens Foo Data
subDataLens = (subData, \ f s -> f { subData = s }) -- short lens definition

Теперь давайте добавим функцию, которая объединяет две линзы:

(#) :: Lens a b -> Lens b c -> Lens a c
(#) (getter1, setter1) (getter2, setter2) =
    (getter2 . getter1, combinedSetter)
    where
      combinedSetter a x =
        let oldInner = getter1 a
            newInner = setter2 oldInner x
        in setter1 a newInner

Код вроде бы быстро пишется, но я думаю, ясно, что он делает: геттеры просто составлены; вы получаете внутреннее значение данных, а затем читаете его поле. Установщик, когда предполагается изменить какое-то значение a на новое значение внутреннего поля x, сначала извлекает старую внутреннюю структуру данных, устанавливает ее внутреннее поле, а затем обновляет внешнюю структуру данных с новой внутренней структурой данных.

Теперь давайте создадим функцию, которая просто увеличивает значение линзы:

increment :: Lens a Int -> a -> a
increment l a = setL l a (getL l a + 1)

Если у нас есть этот код, становится понятно, что он делает:

d = Data 3
print $ increment memberLens d -- Prints "Data 4", the inner field is updated.

Теперь, поскольку мы можем составлять линзы, мы также можем делать это:

f = Foo (Data 5)
print $ increment (subDataLens#memberLens) f
-- Prints "Foo (Data 6)", the innermost field is updated.

Все пакеты линз, по сути, заключают эту концепцию линз - группировку «сеттера» и «получателя» в аккуратную упаковку, которая упрощает их использование. В конкретной реализации линзы можно было бы написать:

with (Foo (Data 5)) $ do
  subDataLens . memberLens $= 7

Итак, вы очень близко подошли к версии кода для C; становится очень легко изменять вложенные значения в дереве структур данных.

Линзы - это не что иное, как простой способ изменения частей некоторых данных. Поскольку из-за них становится намного легче рассуждать об определенных концепциях, они находят широкое применение в ситуациях, когда у вас есть огромные наборы структур данных, которые должны взаимодействовать друг с другом различными способами.

Чтобы узнать о плюсах и минусах линз, см. недавний вопрос здесь, ТАК.

person dflemstr    schedule 28.05.2012
comment
В вашем ответе отсутствует один важный момент: линзы являются первоклассными, поэтому вы можете строить из них другие абстракции. В этом отношении встроенный синтаксис записи не работает. - person jberryman; 29.05.2012
comment
Кроме того, я написал в блоге сообщение об объективах, которое может быть полезно для OP: haskellforall.com/2012/01/ - person Gabriel Gonzalez; 29.05.2012

Линзы предоставляют удобные способы редактирования структур данных единообразным и композиционным способом.

Многие программы построены вокруг следующих операций:

  • просмотр компонента (возможно, вложенной) структуры данных
  • обновление полей (возможно, вложенных) структур данных

Линзы обеспечивают языковую поддержку для просмотра и редактирования структур таким образом, чтобы гарантировать единообразие ваших изменений; что правки могут быть легко составлены; и что тот же код можно использовать для просмотра частей структуры, что и для обновления частей структуры.

Таким образом, линзы позволяют легко писать программы из представлений в структуры; и от структур обратно к представлениям (и редакторам) для этих структур. Они убирают кучу беспорядка, связанного с аксессорами и сеттерами записей.

Pierce et al. популярные линзы, например в их документе Quotient Lenses, а реализации для Haskell теперь широко используются (например, fclabels и средства доступа к данным).

Для конкретных случаев использования рассмотрите:

  • графические пользовательские интерфейсы, в которых пользователь структурированно редактирует информацию
  • парсеры и симпатичные принтеры
  • составители
  • синхронизация обновления структур данных
  • базы данных и схемы

и многие другие ситуации, когда у вас есть модель структуры данных мира и редактируемое представление этих данных.

person Don Stewart    schedule 28.05.2012

В качестве дополнительного примечания часто упускается из виду, что линзы реализуют очень общее понятие «доступ к полю и обновление». Линзы могут быть написаны для всех видов вещей, в том числе для функционально-подобных объектов. Чтобы оценить это, требуется немного абстрактного мышления, поэтому позвольте мне показать вам на примере силы линз:

at :: (Eq a) => a -> Lens (a -> b) b

Используя at, вы можете обращаться к функциям и управлять ими с несколькими аргументами в зависимости от предыдущих аргументов. Просто имейте в виду, что Lens - это категория. Это очень полезная идиома для локальной настройки функций или других вещей.

Вы также можете получить доступ к данным по свойствам или альтернативным представлениям:

polar :: (Floating a, RealFloat a) => Lens (Complex a) (a, a)
mag   :: (RealFloat a) => Lens (Complex a) a

Вы можете пойти дальше, написав линзы, чтобы получить доступ к отдельным полосам сигнала с преобразованием Фурье и многому другому.

person ertes    schedule 28.05.2012