Haskell Language
Lente
Buscar..
Introducción
Lens es una biblioteca para Haskell que proporciona lentes, isomorfismos, pliegues, recorridos, captadores y definidores, lo que expone una interfaz uniforme para consultar y manipular estructuras arbitrarias, no como los conceptos de acceso y mutación de Java.
Observaciones
¿Qué es una lente?
Las lentes (y otras ópticas) nos permiten separarnos describiendo cómo queremos acceder a algunos datos de lo que queremos hacer con ellos. Es importante distinguir entre la noción abstracta de una lente y la implementación concreta. La comprensión abstracta hace que la programación con lens
sea mucho más fácil a largo plazo. Hay muchas representaciones isomorfas de lentes, por lo que para esta discusión evitaremos cualquier discusión de implementación concreta y, en cambio, ofreceremos una visión general de alto nivel de los conceptos.
Enfoque
Un concepto importante en la comprensión abstracta es la noción de enfoque . Las ópticas importantes se centran en una parte específica de una estructura de datos más grande sin olvidar el contexto más amplio. Por ejemplo, la lente _1
enfoca en el primer elemento de una tupla pero no se olvida de lo que había en el segundo campo.
Una vez que tenemos el enfoque, podemos hablar sobre qué operaciones se nos permite realizar con una lente. Dado un Lens sa
que cuando se da un tipo de datos de tipo s
se centra en un determinado a
, podemos ya sea
- Extraiga la
a
olvidando el contexto adicional o - Reemplace el
a
proporcionando un nuevo valor
Estos corresponden a las conocidas operaciones de get
y set
que normalmente se utilizan para caracterizar una lente.
Otras ópticas
Podemos hablar de otras ópticas de manera similar.
Óptico | Se centra en... |
---|---|
Lente | Una parte de un producto |
Prisma | Una parte de una suma |
Travesía | Cero o más partes de una estructura de datos |
Isomorfismo | ... |
Cada óptica se enfoca de una manera diferente, como tal, dependiendo de qué tipo de óptica tengamos podemos realizar diferentes operaciones.
Composición
Además, podemos componer cualquiera de las dos ópticas que hemos discutido hasta ahora para especificar accesos de datos complejos. Los cuatro tipos de ópticas que hemos discutido forman una red, el resultado de componer dos ópticas juntas es su límite superior.
Por ejemplo, si componemos juntos una lente y un prisma, obtenemos un recorrido transversal. La razón de esto es que por su composición (vertical), primero nos enfocamos en una parte de un producto y luego en una parte de una suma. El resultado es una óptica que se centra precisamente en cero o en una parte de nuestros datos, que es un caso especial de un recorrido. (Esto a veces también se llama un recorrido afín).
En haskell
La razón de la popularidad en Haskell es que hay una representación muy breve de la óptica. Todas las ópticas son solo funciones de una cierta forma que se pueden componer juntas utilizando la función de composición. Esto lleva a una incrustación muy ligera que facilita la integración de la óptica en sus programas. Además de esto, debido a los detalles de la codificación, la composición de la función también calcula automáticamente el límite superior de las dos ópticas que componemos. Esto significa que podemos reutilizar los mismos combinadores para diferentes ópticas sin una conversión explícita.
Manipulación de tuplas con lente
Consiguiendo
("a", 1) ^. _1 -- returns "a"
("a", 1) ^. _2 -- returns 1
Ajuste
("a", 1) & _1 .~ "b" -- returns ("b", 1)
Modificando
("a", 1) & _2 %~ (+1) -- returns ("a", 2)
both
transversal
(1, 2) & both *~ 2 -- returns (2, 4)
Lentes para discos
Registro simple
{-# LANGUAGE TemplateHaskell #-}
import Control.Lens
data Point = Point {
_x :: Float,
_y :: Float
}
makeLenses ''Point
Lentes x
e y
son creados.
let p = Point 5.0 6.0
p ^. x -- returns 5.0
set x 10 p -- returns Point { _x = 10.0, _y = 6.0 }
p & x +~ 1 -- returns Point { _x = 6.0, _y = 6.0 }
Gestión de registros con nombres de campos repetidos
data Person = Person { _personName :: String }
makeFields ''Person
Crea una clase de tipo HasName
, name
lente para Person
, y convierte a Person
una instancia de HasName
. Los registros subsiguientes también se agregarán a la clase:
data Entity = Entity { _entityName :: String }
makeFields ''Entity
Se requiere la extensión Template Haskell para que makeFields
funcione. Técnicamente, es totalmente posible crear las lentes hechas de esta manera a través de otros medios, por ejemplo, a mano.
Lentes de estado
Los operadores de lentes tienen variantes útiles que operan en contextos con estado. Se obtienen reemplazando ~
con =
en el nombre del operador.
(+~) :: Num a => ASetter s t a a -> a -> s -> t
(+=) :: (MonadState s m, Num a) => ASetter' s a -> a -> m ()
Nota: No se espera que las variantes con estado cambien el tipo, por lo que tienen las firmas de la
Lens'
o laSimple Lens'
.
Deshacerse de &
cadenas
Si es necesario encadenar las operaciones con lentes, a menudo se ve así:
change :: A -> A
change a = a & lensA %~ operationA
& lensB %~ operationB
& lensC %~ operationC
Esto funciona gracias a la asociatividad de &
. Sin embargo, la versión con estado es más clara.
change a = flip execState a $ do
lensA %= operationA
lensB %= operationB
lensC %= operationC
Si lensX
es realmente id
, toda la operación puede, por supuesto, ejecutarse directamente simplemente levantándola con modify
.
Código imperativo con estado estructurado.
Asumiendo este estado de ejemplo:
data Point = Point { _x :: Float, _y :: Float }
data Entity = Entity { _position :: Point, _direction :: Float }
data World = World { _entities :: [Entity] }
makeLenses ''Point
makeLenses ''Entity
makeLenses ''World
Podemos escribir código que se asemeja a los lenguajes imperativos clásicos, mientras nos permite usar los beneficios de Haskell:
updateWorld :: MonadState World m => m ()
updateWorld = do
-- move the first entity
entities . ix 0 . position . x += 1
-- do some operation on all of them
entities . traversed . position %= \p -> p `pointAdd` ...
-- or only on a subset
entities . traversed . filtered (\e -> e ^. position.x > 100) %= ...
Escribiendo una lente sin plantilla Haskell
Para desmitificar Template Haskell, supongamos que tienes
data Example a = Example { _foo :: Int, _bar :: a }
entonces
makeLenses 'Example
produce (más o menos)
foo :: Lens' (Example a) Int bar :: Lens (Example a) (Example b) a b
Sin embargo, no hay nada particularmente mágico en marcha. Puedes escribirlas tú mismo:
foo :: Lens' (Example a) Int -- :: Functor f => (Int -> f Int) -> (Example a -> f (Example a)) ;; expand the alias foo wrap (Example foo bar) = fmap (\newFoo -> Example newFoo bar) (wrap foo) bar :: Lens (Example a) (Example b) a b -- :: Functor f => (a -> f b) -> (Example a -> f (Example b)) ;; expand the alias bar wrap (Example foo bar) = fmap (\newBar -> Example foo newBar) (wrap bar)
Esencialmente, usted quiere "visitar" el "enfoque" de su lente con la función de wrap
y luego reconstruir el tipo "completo".
Lente y prisma
Una Lens' sa
significa que siempre puedes encontrar una a
dentro de cualquier s
. Un Prism' sa
significa que a veces se puede encontrar que s
en realidad sólo es a
pero a veces es otra cosa.
Para ser más claros, tenemos _1 :: Lens' (a, b) a
porque cualquier tupla siempre tiene un primer elemento. Tenemos _Just :: Prism' (Maybe a) a
porque a veces Maybe a
es en realidad una a
valor envuelto en Just
pero a veces es Nothing
.
Con esta intuición, algunos combinadores estándar se pueden interpretar paralelos entre sí.
-
view :: Lens' sa -> (s -> a)
"obtiene" laa
fuera de las
-
set :: Lens' sa -> (a -> s -> s)
"conjuntos" de laa
ranura ens
-
review :: Prism' sa -> (a -> s)
"se da cuenta" de que unaa
podría ser unas
-
preview :: Prism' sa -> (s -> Maybe a)
"intenta" convertir uns
en una
.
Otra forma de pensarlo es que un valor de tipo Lens' sa
demuestra que s
tiene la misma estructura que (r, a)
para algunos r
desconocidos. Por otro lado, Prism' sa
s
demuestra que s
tiene la misma estructura que Either ra
para alguna r
. Podemos escribir las cuatro funciones anteriores con este conocimiento:
-- `Lens' s a` is no longer supplied, instead we just *know* that `s ~ (r, a)` view :: (r, a) -> a view (r, a) = a set :: a -> (r, a) -> (r, a) set a (r, _) = (r, a) -- `Prism' s a` is no longer supplied, instead we just *know* that `s ~ Either r a` review :: a -> Either r a review a = Right a preview :: Either r a -> Maybe a preview (Left _) = Nothing preview (Right a) = Just a
Travesías
Un Traversal' sa
muestra que s
tiene 0-a-muchos a
s dentro de ella.
toListOf :: Traversal' s a -> (s -> [a])
Cualquier tipo t
que sea Traversable
tiene automáticamente ese traverse :: Traversal (ta) a
.
Podemos utilizar un Traversal
para establecer o mapa sobre todos ellos a
valores
> set traverse 1 [1..10]
[1,1,1,1,1,1,1,1,1,1]
> over traverse (+1) [1..10]
[2,3,4,5,6,7,8,9,10,11]
A f :: Lens' sa
dice que hay exactamente una a
dentro de s
. A g :: Prism' ab
dice que hay 0 o 1 b
s en a
. Componiendo f . g
nos da un Traversal' sb
porque a continuación f
g
muestra cómo hay 0 a 1 b
s en s
.
Lentes componen
Si tienes un f :: Lens' ab
y un g :: Lens' bc
entonces f . g
es Lens' ac
una Lens' ac
al seguir f
primero y luego g
. Notablemente:
- Lentes de componer como funciones (en realidad sólo son funciones)
- Si piensa en la funcionalidad de
view
de laLens
, parece que los datos fluyen "de izquierda a derecha": esto puede parecerle a su intuición normal para la composición de la función. Por otro lado, debería sentirse natural si piensas en ello.
-Nota como ocurre en los idiomas OO.
Más que solo componer Lens
with Lens
, (.)
Se puede usar para componer casi cualquier tipo " Lens
like". No siempre es fácil ver cuál es el resultado, ya que el tipo se vuelve más difícil de seguir, pero puede usar la tabla de lens
para averiguarlo. La composición x . y
tiene el tipo del límite mínimo superior de los tipos tanto de x
como de y
en ese gráfico.
Lentes con clase
Además de la función estándar de makeLenses
para generar Lens
, Control.Lens.TH
también ofrece la función makeClassy
. makeClassy
tiene el mismo tipo y funciona esencialmente de la misma manera que makeLenses
, con una diferencia clave. Además de generar las lentes y recorridos estándar, si el tipo no tiene argumentos, también creará una clase que describa todos los tipos de datos que poseen el tipo como campo. Por ejemplo
data Foo = Foo { _fooX, _fooY :: Int }
makeClassy ''Foo
creará
class HasFoo t where
foo :: Simple Lens t Foo
instance HasFoo Foo where foo = id
fooX, fooY :: HasFoo t => Simple Lens t Int
Campos con makeFields
(Este ejemplo copiado de esta respuesta StackOverflow )
Digamos que tiene varios tipos de datos diferentes que todos deberían tener una lente con el mismo nombre, en este caso la capacity
. La makeFields
creará una clase que logrará esto sin conflictos de espacio de nombres.
{-# LANGUAGE FunctionalDependencies
, MultiParamTypeClasses
, TemplateHaskell
#-}
module Foo
where
import Control.Lens
data Foo
= Foo { fooCapacity :: Int }
deriving (Eq, Show)
$(makeFields ''Foo)
data Bar
= Bar { barCapacity :: Double }
deriving (Eq, Show)
$(makeFields ''Bar)
Luego en ghci:
*Foo
λ let f = Foo 3
| b = Bar 7
|
b :: Bar
f :: Foo
*Foo
λ fooCapacity f
3
it :: Int
*Foo
λ barCapacity b
7.0
it :: Double
*Foo
λ f ^. capacity
3
it :: Int
*Foo
λ b ^. capacity
7.0
it :: Double
λ :info HasCapacity
class HasCapacity s a | s -> a where
capacity :: Lens' s a
-- Defined at Foo.hs:14:3
instance HasCapacity Foo Int -- Defined at Foo.hs:14:3
instance HasCapacity Bar Double -- Defined at Foo.hs:19:3
Entonces, lo que realmente se hace se declara una clase HasCapacity sa
, donde la capacidad es un Lens'
de s
a a
( a
se fija una vez que se conoce s). Descubrió el nombre "capacidad" al eliminar el nombre (en minúsculas) del tipo de datos del campo; Me resulta agradable no tener que usar un guión bajo ni en el nombre del campo ni en el de la lente, ya que a veces la sintaxis de grabación es lo que usted desea. Puede usar makeFieldsWith y las diferentes Reglas de la lente para tener algunas opciones diferentes para calcular los nombres de las lentes.
En caso de que ayude, use ghci -ddump-splices Foo.hs:
[1 of 1] Compiling Foo ( Foo.hs, interpreted )
Foo.hs:14:3-18: Splicing declarations
makeFields ''Foo
======>
class HasCapacity s a | s -> a where
capacity :: Lens' s a
instance HasCapacity Foo Int where
{-# INLINE capacity #-}
capacity = iso (\ (Foo x_a7fG) -> x_a7fG) Foo
Foo.hs:19:3-18: Splicing declarations
makeFields ''Bar
======>
instance HasCapacity Bar Double where
{-# INLINE capacity #-}
capacity = iso (\ (Bar x_a7ne) -> x_a7ne) Bar
Ok, modules loaded: Foo.
Así que el primer empalme hizo la clase HasCapcity
y agregó una instancia para Foo; el segundo usó la clase existente e hizo una instancia para Bar.
Esto también funciona si importa la clase HasCapcity
desde otro módulo; makeFields
puede agregar más instancias a la clase existente y distribuir sus tipos en múltiples módulos. Pero si lo usa de nuevo en otro módulo donde no haya importado la clase, creará una nueva clase (con el mismo nombre) y tendrá dos lentes de capacidad sobrecargadas que no son compatibles.