haskell - descripcion - ¿Para qué son útiles las lentes?
meta title html (3)
Parece que no puedo encontrar ninguna explicación de para qué lentes se usan en ejemplos prácticos. Este breve párrafo de la página de Hackage es el más cercano que he encontrado:
Este módulo proporciona una forma conveniente de acceder y actualizar los elementos de una estructura. Es muy similar a Data.Accessors, pero es un poco más genérico y tiene menos dependencias. Particularmente me gusta cuán limpiamente maneja las estructuras anidadas en las mónadas estatales.
Entonces, ¿para qué se usan? ¿Qué beneficios y desventajas tienen sobre otros métodos? ¿Por qué son necesarios?
Como nota adicional, a menudo se pasa por alto que los lentes implementan una noción muy genérica de "acceso de campo y actualización". Las lentes se pueden escribir para todo tipo de cosas, incluidos los objetos similares a funciones. Requiere un poco de pensamiento abstracto para apreciar esto, así que déjame mostrarte un ejemplo del poder de las lentes:
at :: (Eq a) => a -> Lens (a -> b) b
El uso at
usted puede acceder y manipular funciones con múltiples argumentos dependiendo de los argumentos anteriores. Solo tenga en cuenta que Lens
es una categoría. Esta es una expresión muy útil para ajustar localmente funciones u otras cosas.
También puede acceder a los datos por propiedades o representaciones alternativas:
polar :: (Floating a, RealFloat a) => Lens (Complex a) (a, a)
mag :: (RealFloat a) => Lens (Complex a) a
Puede ir más allá escribiendo lentes para acceder a bandas individuales de una señal transformada por Fourier y mucho más.
Las lentes proporcionan formas convenientes de editar estructuras de datos, de una manera uniforme y compositiva.
Muchos programas se basan en las siguientes operaciones:
- ver un componente de una estructura de datos (posiblemente anidada)
- actualizar campos de estructuras de datos (posiblemente anidadas)
Las lentes brindan soporte de idiomas para ver y editar estructuras de una manera que garantice que sus ediciones sean consistentes; que las ediciones se pueden componer fácilmente; y que el mismo código se puede usar para ver partes de una estructura, como para actualizar las partes de la estructura.
Las lentes facilitan la escritura de programas desde las vistas hacia las estructuras; y desde las estructuras a las vistas (y editores) para esas estructuras. Ellos limpian gran parte del desorden de los usuarios y instaladores de registros.
Pierce et al. las lentes popularizadas, por ejemplo, en su documento Lentes de cociente , y las implementaciones de Haskell son ahora ampliamente utilizadas (por ejemplo, fclabels y fclabels datos).
Para casos de uso concretos, considere:
- interfaces gráficas de usuario, donde un usuario está editando información de una manera estructurada
- analizadores e impresoras bonitas
- compiladores
- sincronizando las estructuras de datos de actualización
- bases de datos y esquemas
y muchas otras situaciones en las que tiene un modelo de estructura de datos del mundo y una vista editable de esos datos.
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 se suelen llamar "campos"). ) Esto lleva 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;
A continuación, puede crear valores de este tipo recién definido, como por ejemplo:
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;
Del 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 tiene una matriz como la anterior en Haskell y desea cambiar la celda superior derecha en la matriz, debe escribir esto:
mat2 = mat { matCol2 = (matCol2 mat) { vecX = 4 } }
Funciona, pero se ve 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, puede 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)
Hay muchas formas de implementar lentes; para este texto, digamos que una lente es como la de arriba:
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
, por lo que memberLens
arriba sería un Lens Data Int
. ¿Qué nos permite esto?
Bueno, primero hagamos dos funciones simples que extraigan los getters y setters 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 comenzar a abstraer sobre cosas. Vayamos a la situación anterior de nuevo, que queremos modificar un valor "de dos pisos de profundidad". Agregamos 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, agreguemos 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 se escribió rápidamente, pero creo que está claro lo que hace: los captadores se componen simplemente; 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 de 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, hagamos una función que simplemente incremente 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, queda claro lo que hace:
d = Data 3
print $ increment memberLens d -- Prints "Data 4", the inner field is updated.
Ahora, debido a que podemos 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 hacen todos los paquetes de lentes es esencialmente envolver 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 de lente particular, uno podría escribir:
with (Foo (Data 5)) $ do
subDataLens . memberLens $= 7
Entonces, te acercas mucho a 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 una pregunta reciente aquí en SO .