haskell clojure functional-programming lens zipper

haskell - ¿Cuáles son las diferencias entre lentes y cremalleras?



clojure functional-programming (3)

Este es un ejemplo del uso de una cremallera en Haskell:

data Tree a = Fork (Tree a) (Tree a) | Leaf a data Cxt a = Top | L (Cxt a) (Tree a) | R (Tree a) (Cxt a) type Loc a = (Tree a, Cxt a) left :: Loc a -> Loc a left (Fork l r, c) = (l, L c r) right :: Loc a -> Loc a right (Fork l r, c) = (r, R l c) top :: Tree a -> Loc a top t = (t, Top) up :: Loc a -> Loc a up (t, L c r) = (Fork t r, c) up (t, R l c) = (Fork l t, c) upmost :: Loc a -> Loc a upmost l@(t, Top) = l upmost l = upmost (up l) modify :: Loc a -> (Tree a -> Tree a) -> Loc a modify (t, c) f = (f t, c)

Este es un ejemplo del uso de una cremallera en Clojure:

(use ''clojure.zip) (require ''[clojure.zip :as z]) user> (def z [[1 2 3] [4 [5 6] 7] [8 9]]) #''user/z user> (def zp (zipper vector? seq (fn [_ c] c) z)) #''user/zp user> zp [[[1 2 3] [4 [5 6] 7] [8 9]] nil] user> (-> zp down) [[1 2 3] {:l [], :pnodes [[[1 2 3] [4 [5 6] 7] [8 9]]], :ppath nil, :r ([4 [5 6] 7] [8 9])}] user> (first (-> zp down)) [1 2 3]

Este es un ejemplo del uso de una lente en Haskell:

data Person = P { name :: String , addr :: Address } data Address = A { street :: String , city :: String , postcode :: String } setPostcode :: String -> Person -> Person setPostcode pc p = p { addr = addr p { postcode = pc }}

Este es un ejemplo del uso de una lente en Clojure.

(use ''lens) (defrecord Address [street city postcode]) (defrecord Person [name age address]) (defrecord User [uid username identity password]) (def -postcode (mklens :postcode)) (def -city (mklens :city)) (def -street (mklens :street)) (def -address (mklens :address)) (def -age (mklens :age)) (def -name (mklens :name)) (def -uid (mklens :uid)) (def -username (mklens :username)) (def -identity (mklens :identity)) (def -password (mklens :password)) (-get -postcode home) (-set -postcode home 500)

Ahora parece que tanto las lentes como las cremalleras son formas funcionales de atravesar estructuras de datos anidadas.

Mi pregunta es: ¿Cuáles son las diferencias entre lentes y cremalleras? ¿Es una adecuada para un caso de uso particular?


Las cremalleras son similares a los cursores: permiten atravesar los árboles de forma ordenada . Sus operaciones habituales son up , down , left , right y edit . (Los nombres pueden variar dependiendo de impl.)

Las lentes son una especie de claves generalizadas (como en "claves de una estructura de datos asociativa"). La estructura no necesita ser ordenada. Sus operaciones habituales son de fetch y putback y son muy similares a get y assoc . (Los nombres pueden variar dependiendo de impl.)

Entonces, como ven, las cremalleras están muy preocupadas por la jerarquía (arriba / abajo) y el orden (izquierda / derecha), mientras que las lentes solo se enfocan (de ahí el nombre) en un dato, que incluso puede ser una proyección (eso es algo que no existía por sí solo en la estructura original).

Por ejemplo, en mi trabajo en curso en Enliven , tengo lentes que me permiten concentrarme en una sola clase o atributo de estilo en un documento HTML.


Las cremalleras son una variante de un tipo de datos que despliega el tipo en su contexto local y su extensión en todas las direcciones. Encima de una cremallera puede implementar un movimiento eficiente y una actualización local .

Las lentes son exámenes de primera clase de un componente particular de un tipo de datos. Se centran en 0, 1 o muchas subpartes de una estructura de datos. Cabe destacar que su ejemplo de lente en Haskell no es en realidad una lente, no es de primera clase.

Es perfectamente razonable construir una lente que se enfoque en alguna parte de la cremallera. Por ejemplo, una cremallera aún más simple que sus ejemplos es una cremallera de lista de contras

data Cons a = Empty | Cons a (Cons a) data ConsZ a = ConsZ { lefts :: Cons a; here :: a; rights :: Cons a } zip :: Cons a -> Maybe (ConsZ a) zip Empty = Nothing zip (Cons a as) = ConsZ Empty a as unzip :: ConsZ a -> Cons a unzip (ConsZ Empty a as) = Cons a as unzip (ConsZ (Cons l ls) a as) = unzip (ConsZ ls) l (Cons a as)

Podemos modificar esta estructura incrementalmente, moviendo el foco hacia la izquierda o hacia la derecha.

moveRight :: ConsZ a -> Maybe (ConsZ a) moveRight (ConsZ _ _ Empty) = Nothing moveRight (ConsZ ls x (Cons a as)) = ConsZ (Cons x ls) a as

y modificar el punto local actual

modify :: (a -> a) -> ConsZ a -> ConsZ a modify f (ConsZ ls x rs) = ConsZ ls (f x) rs

También podemos construir lentes que acceden a cada parte de la estructura de la cremallera.

type Lens s a = forall f . Functor f => (a -> f a) -> (s -> f s) _lefts :: Lens (ConsZ a) a _lenfs inj (ConsZ ls x rs) = (/ls -> ConsZ ls'' x rs) <$> inj ls _here :: Lens (ConsZ a) a _here inj (ConsZ ls x rs) = (/x'' -> ConsZ ls x'' rs) <$> inj x

E incluso utilizarlos para construir nuestras acciones de cremallera con eficacia

over :: ((a -> Identity a) -> s -> Identity s) -> (a -> a) -> (s -> s) over l f s = runIdentity (l (Identity . f) s) modify = over _here

En última instancia, sin embargo, una lente siempre es un acceso de primera clase a un punto en particular en una estructura de datos. Se pueden componer, lo que da la ilusión de "movimiento" en un tipo, pero si realmente quieres eso, debes hacer la transformación de la cremallera y utilizar un tipo de cremallera real.


Las lentes y las cremalleras no son formas de mirar el mundo que se excluyen mutuamente. Puede construir un tipo de datos de "enfoque móvil" sobre las lentes, al verificar una cadena de lentes como una pila de tipo alineado. Hacer un seguimiento de los tipos que visitó en su camino hacia una estructura significa que sabe qué tipos verá cuando vuelva a subir.

La API de este "enfoque móvil" se parece aproximadamente a esto:

empty :: Path (E :> a) up :: Path (as :> a :> b) -> Path (as :> a) down :: Path (as :> a) -> Traversal'' a b -> Path (as :> a :> b) left :: Path (as :> a :> b) -> Path (as :> a :> b) right :: Path (as :> a :> b) -> Path (as :> a :> b) flatten :: Path as -> Traversal'' (Top as) (Bottom as)

Path está parametrizada por una lista de tipos de snoc. El tipo de "foco actual" de la Path es el elemento más a la derecha de la lista.

Dado un Path que se enfoca en una a en alguna estructura, puede usar down para agregar un Traversal'' ab , para recuperar un Path que se enfoca en una b (es decir, el primer resultado del Traversal ). Luego puede volver a up , que muestra el Traversal más recientemente agregado para devolverle un Path que se enfoca en un nuevo. left y right mueven el foco dentro del Traversal superior.

También necesita una forma de convertir un Path en un Traversal real, para poder acceder al valor real en el que se enfoca su Path . El combinador flatten hace esto. Top y Bottom son un par de familias de tipos que encuentran los elementos de más a la izquierda y más a la derecha de una lista snoc, respectivamente.

Entonces, ¿cómo se implementa?

infixl 5 :> data Snoc a = E | Snoc a :> a type family Top as where Top (E :> a) = a Top (as :> _) = Top as type family Bottom as where Bottom (_ :> a) = a data Path as where Top :: Path (E :> a) Child :: Path (as :> a) -> Traversal'' a b -> Int -> Path (as :> a :> b)

Path es un GADT en forma de pila. El constructor Top crea una Path vacía, una ruta desde cualquier valor hacia sí misma. El constructor Child enfoca en un elemento particular de un Traversal : contiene el Path padre que se enfoca en una a , un Traversal de a a b , y un Int representa el elemento particular del Traversal que se enfoca el Path .

empty crea un camino vacío.

empty :: Path (E :> a) empty = Top

up toma una ruta no vacía (garantizada por el tipo) y muestra el Traversal superior de ella.

up :: Path (as :> a :> b) -> Path (as :> a) up (Child p _ _) = p

down toma un Traversal y lo agrega a un Path , enfocándose en el resultado más a la izquierda del Traversal .

down :: Path (as :> a) -> Traversal'' a b -> Path (as :> a :> b) down p t = Child p t 0

left y la right no cambian el nivel de la estructura en la que te estás enfocando, sin agregar o eliminar recorridos de la pila, simplemente cambian a qué elemento del recorrido más alto al que apunta la ruta.

left :: Path (as :> a :> b) -> Path (as :> a :> b) left (Child p t n) = Child p t (n - 1) right :: Path (as :> a :> b) -> Path (as :> a :> b) right (Child p t n) = Child p t (n + 1)

flatten mira cada recorrido a su vez y usa elementOf para enfocarse en un elemento particular del recorrido. Los compone todos juntos usando . .

flatten :: Path as -> Traversal'' (Top as) (Bottom as) flatten Top = id flatten (Child p t n) = flatten p . elementOf t n

Path no es una cremallera, exactamente. Una parte importante de lo que hace que una cremallera sea una cremallera es que puede ver o editar eficientemente el enfoque y sus vecinos sin atravesar o reconstruir todo. Path simplemente compone recorridos sin hacer referencia a una estructura particular, por lo que es tan ineficiente como trabajar con recorridos completos.

Sin embargo, no es un gran salto de Path a una verdadera cremallera. El paquete de zippers proporciona verdaderas cremalleras (cursores con acceso eficiente a una parte enfocada de una estructura real) genéricamente, basándose en esta idea de una secuencia de lentes alineada con el tipo. A medida que desciende a través de una estructura, la Zipper desempaqueta cada recorrido en una estructura de datos como su Loc . Luego, cuando retrocedes upward vuelve a escribir los nuevos valores en la estructura utilizando Traversal .