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

  1. Extraiga la a olvidando el contexto adicional o
  2. 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.

introduzca la descripción de la imagen aquí

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 la Simple 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" la a fuera de la s
  • set :: Lens' sa -> (a -> s -> s) "conjuntos" de la a ranura en s
  • review :: Prism' sa -> (a -> s) "se da cuenta" de que una a podría ser una s
  • preview :: Prism' sa -> (s -> Maybe a) "intenta" convertir un s en un a .

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 la Lens , 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.



Modified text is an extract of the original Stack Overflow Documentation
Licenciado bajo CC BY-SA 3.0
No afiliado a Stack Overflow