Ofrecen una abstracción clara sobre las actualizaciones de datos, y nunca son realmente "necesarias". Simplemente te dejan razonar sobre un problema de una manera diferente.
En algunos lenguajes de programación imperativos/"orientados a objetos" como C, tiene el concepto familiar de una colección de valores (llamémoslos "estructuras") y formas de etiquetar cada valor en la colección (las etiquetas son típicamente llamado "campos").Esto conduce a una definición como esta:
typedef struct { /* defining a new struct type */
float x; /* field */
float y; /* field */
} Vec2;
typedef struct {
Vec2 col1; /* nested structs */
Vec2 col2;
} Mat2;
continuación, puede crear valores de este tipo que acaba de definir de este modo:
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;
mismo modo en Haskell, tenemos tipos de datos:
data Vec2 =
Vec2
{ vecX :: Float
, vecY :: Float
}
data Mat2 =
Mat2
{ matCol1 :: Vec2
, matCol2 :: Vec2
}
Este tipo de datos se usa así:
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
Sin embargo, en Haskell, no hay una manera fácil de cambiar los campos anidados en una estructura de datos. Esto se debe a que necesita volver a crear todos los objetos de envoltura alrededor del valor que está cambiando, porque los valores de Haskell son inmutables. Si usted tiene una matriz como la anterior en Haskell, y desea cambiar la celda superior derecha de la matriz, que tener que escribir esto:
mat2 = mat { matCol2 = (matCol2 mat) { vecX = 4 } }
Funciona, pero parece torpe. Entonces, lo que se le ocurrió a alguien es básicamente esto: si agrupa dos cosas juntas: el "captador" de un valor (como vecX
y matCol2
anterior) con una función correspondiente que, dada la estructura de datos a la que pertenece el captador, crea una nueva estructura de datos con ese valor cambiado, puedes hacer muchas cosas ordenadas. Por ejemplo:
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)
Existen muchas formas de implementar lentes; para este texto, digamos que una lente es igual que el anterior:
type Lens a b = (a -> b, a -> b -> a)
es decir, es la combinación de un getter y un setter para algún tipo a
que tiene un campo de tipo b
, entonces memberLens
arriba sería Lens Data Int
. ¿Qué nos permite esto?
Bueno, primero vamos a hacer dos funciones simples que extraen los captadores y definidores de una lente:
getL :: Lens a b -> a -> b
getL (getter, setter) = getter
setL :: Lens a b -> a -> b -> a
setL (getter, setter) = setter
Ahora, podemos empezar abstracción sobre la materia. Vayamos a la situación anterior de nuevo, que queremos modificar un valor "de dos pisos de profundidad". Añadimos una estructura de datos con otra lente:
data Foo = Foo { subData :: Data }
subDataLens :: Lens Foo Data
subDataLens = (subData, \ f s -> f { subData = s }) -- short lens definition
Ahora, vamos a añadir una función que compone dos lentes:
(#) :: 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
El código es una especie de forma rápida escrito, pero creo que está claro que lo que hace : los getters son simplemente compuestos; obtienes el valor interno de los datos, y luego lees su campo. El colocador, cuando se supone que altera algún valor a
con el nuevo valor de campo interno x
, primero recupera la estructura de datos interna anterior, establece su campo interno y luego actualiza la estructura de datos externa con la nueva estructura de datos interna.
Ahora, vamos a hacer una función que simplemente incrementa el valor de una lente:
increment :: Lens a Int -> a -> a
increment l a = setL l a (getL l a + 1)
Si tenemos este código, se pone de manifiesto lo que hace:
d = Data 3
print $ increment memberLens d -- Prints "Data 4", the inner field is updated.
Ahora, debido a que puede componer lentes, también podemos hacer esto:
f = Foo (Data 5)
print $ increment (subDataLens#memberLens) f
-- Prints "Foo (Data 6)", the innermost field is updated.
Lo que todos los paquetes de lentes hacen es esencialmente w rap este concepto de lentes, la agrupación de un "setter" y un "getter", en un paquete ordenado que los hace fáciles de usar. En una implementación lente en particular, uno sería capaz de escribir:
with (Foo (Data 5)) $ do
subDataLens . memberLens $= 7
Así, se obtiene muy cerca de la versión C del código; se vuelve muy fácil modificar los valores anidados en un árbol de estructuras de datos.
Las lentes no son más que esto: una manera fácil de modificar partes de algunos datos. Debido a que cada vez es más fácil razonar sobre ciertos conceptos debido a ellos, ven un amplio uso en situaciones en las que tiene grandes conjuntos de estructuras de datos que tienen que interactuar entre sí de varias maneras.
Para conocer los pros y los contras de las lentes, consulte a recent question here on SO.
Puede disfrutar viendo las [Lentes: un imperativo funcional] de Edward Kmett (http://www.youtube.com/watch?v=efv0SQNde5Q) hablar. Se presenta en Scala, pero la traducción a la utilidad de las lentes en Haskell debe ser clara. –