Они предлагают чистую абстракцию по поводу обновлений данных и никогда не «нужны». Они просто позволяют вам рассуждать о проблеме по-другому.
В некоторых императивных / «объектно-ориентированных» языках программирования, таких как 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