Al manipular estructuras de datos inmutables, ¿cuál es la diferencia entre la asociación de Clojure y las lentes de Haskell?
functional-programming immutability (4)
Estás hablando de dos cosas muy diferentes.
Puede usar lens para resolver problemas similares como assoc-in, donde está usando tipos de colección ( Data.Map
, Data.Vector
) que coinciden con la semántica, pero hay diferencias.
En idiomas no tipificados como Clojure, es común estructurar los datos de su dominio en términos de colecciones que tienen contenidos no estáticos (hash-maps, vectores, etc.) incluso cuando se trata de datos de modelado que son convencionalmente estáticos.
En Haskell, estructuraría sus datos utilizando un registro y ADT, donde, si bien puede expresar contenidos que pueden o no existir (o envolver una colección), el contenido predeterminado es el contenido conocido de forma estática.
Una biblioteca para consultar sería http://hackage.haskell.org/package/lens-aeson donde tiene documentos JSON que tienen contenidos posiblemente variables.
Los ejemplos demuestran que cuando su ruta y tipo no coinciden con la estructura / los datos, arroja un valor Nothing
lugar de Just a
.
La lente no hace nada más que proporcionar un comportamiento de captador / ajustador de sonido. No expresa una expectativa particular acerca de cómo se ven sus datos, mientras que assoc-in solo tiene sentido con colecciones asociativas con contenidos posiblemente no deterministas.
Otra diferencia aquí es la pureza y la pereza frente a la semántica estricta e impura. En Haskell, si nunca usó los estados "más antiguos", y solo el más reciente, entonces solo se realizará ese valor.
Las lentes dr, tal como se encuentran en Lens
y otras bibliotecas similares, son más generales, más útiles, de tipo seguro y especialmente agradables en lenguajes PF perezosos / puros.
Necesito manipular y modificar colecciones inmutables profundamente anidadas (mapas y listas), y me gustaría entender mejor los diferentes enfoques. Estas dos bibliotecas resuelven más o menos el mismo problema, ¿verdad? ¿En qué se diferencian? ¿Para qué tipo de problemas es más adecuado un enfoque que el otro?
Esta pregunta es algo similar a preguntar cuál es la diferencia entre las mónadas de Clojure y Haskell. Voy a imitar las respuestas hasta ahora: seguro es como una mónada de la List
, pero las mónadas son mucho más genéricas y poderosas.
Pero, esto es un poco tonto, ¿verdad? Las mónadas se han implementado en Clojure. ¿Por qué no se usan todo el tiempo? Clojure tiene en su núcleo una filosofía diferente acerca de cómo manejar el estado, pero aún se siente libre para tomar prestadas buenas ideas de grandes idiomas como Haskell en sus bibliotecas.
Entonces, seguro, assoc-in
, get-in
, update-in
, etc. son una especie de lentes para estructuras de datos asociativos. Y hay implementaciones de lentes en general en Clojure por ahí. ¿Por qué no se usan todo el tiempo? Es una diferencia en filosofía (y tal vez la extraña sensación de que con todos los setters y getters estaríamos haciendo otro Java dentro de Clojure y de alguna manera terminaríamos casándonos con nuestra madre). Pero, Clojure se siente libre de tomar prestadas buenas ideas, y puedes ver enfoques inspirados en lentes que se abren camino en proyectos geniales como Om y Enliven.
Debes tener cuidado al hacer tales preguntas, porque como los medio hermanos que ocupan parte del mismo espacio, Clojure y Haskell están obligados a pedir prestado unos a otros y discutir un poco acerca de quién tiene la razón.
La assoc-in
de Clojure le permite especificar una ruta a través de una estructura de datos anidada usando enteros y palabras clave e introducir un nuevo valor en esa ruta. Tiene socios dissoc-in
, get-in
y update-in
que eliminan elementos, los obtienen sin eliminarlos o los modifican respectivamente.
Las lentes son una noción particular de la programación bidireccional donde se especifica un enlace entre dos fuentes de datos y ese enlace le permite reflejar las transformaciones de una a otra. En Haskell, esto significa que puede crear lentes o valores similares a lentes que conectan una estructura de datos completa a algunas de sus partes y luego usarlas para transmitir cambios de las partes a la totalidad.
Aquí hay una analogía. Si nos fijamos en un uso de assoc-in
está escrito como
(assoc-in whole path subpart)
y podríamos obtener cierta información al pensar en el path
como una lente y assoc-in
como un combinador de lentes. De forma similar, puede escribir (utilizando el paquete de lens
Haskell)
set lens subpart whole
para que conectemos assoc-in
con set
y path
con lens
. También podemos completar la tabla.
set assoc-in
view get-in
over update-in
(unneeded) dissoc-in -- this is special because `at` and `over`
-- strictly generalize dissoc-in
Eso es un comienzo para las similitudes, pero también hay una gran diferencia. En muchos sentidos, la lens
es mucho más genérica que la familia *-in
de funciones de Clojure. Normalmente, esto no es un problema para Clojure porque la mayoría de los datos de Clojure se almacenan en estructuras anidadas hechas de listas y diccionarios. Haskell usa muchos más tipos personalizados muy libremente y su sistema de tipos refleja información sobre ellos. Las lentes generalizan la familia de funciones *-in
porque funcionan sin problemas en un dominio mucho más complejo.
Primero, incrustemos tipos de Clojure en Haskell y escribamos la familia de funciones *-in
.
type Dict a = Map String a
data Clj
= CljVal -- Dynamically typed Clojure value,
-- not an array or dictionary
| CljAry [Clj] -- Array of Clojure types
| CljDict (Dict Clj) -- Dictionary of Clojure types
makePrisms ''''Clj
Ahora podemos usar set
as assoc-in
casi directamente.
(assoc-in whole [1 :foo :bar 3] part)
set ( _CljAry . ix 1
. _CljDict . ix "foo"
. _CljDict . ix "bar"
. _CljAry . ix 3
) part whole
Obviamente, esto tiene mucho más ruido sintáctico, pero denota un grado más alto de testimonio explícito sobre lo que significa el "camino" hacia un tipo de datos, en particular, denota si estamos descendiendo a una matriz o un diccionario. Podríamos, si quisiéramos, eliminar parte de ese ruido adicional al crear Clj
instancia de Clj
en la clase de Ixed
Haskell Ixed
, pero en este punto apenas vale la pena.
En cambio, lo que se debe hacer es que la assoc-in
está aplicando a un tipo muy particular de descenso de datos. Es más general que los tipos que IFn
anteriormente debido a la tipificación dinámica y la sobrecarga de IFn
de IFn
, pero una estructura fija muy similar como esa podría incrustarse en Haskell con poco esfuerzo.
Sin embargo, las lentes pueden ir mucho más lejos y hacerlo con mayor seguridad de tipo. Por ejemplo, el ejemplo anterior en realidad no es un verdadero "Lens", sino un "Prisma" o "Traversal" que permite al sistema de tipos identificar de manera estática la posibilidad de no realizar ese recorrido. Nos obligará a pensar en condiciones de error como esas (incluso si optamos por ignorarlas).
Lo que es importante es que podemos estar seguros de que, cuando tenemos una lente verdadera, el descenso del tipo de datos no puede fallar, ese tipo de garantía es imposible de hacer en Clojure.
Podemos definir tipos de datos personalizados y hacer lentes personalizados que desciendan a ellos de una manera segura.
data Point =
Point { _latitude :: Double
, _longitude :: Double
, _meta :: Map String String }
deriving Show
makeLenses ''''Point
> let p0 = Point 0 0
> let p1 = set latitude 3 p0
> view latitude p1
3.0
> view longitude p1
0.0
> let p2 = set (meta . ix "foo") "bar" p1
> preview (meta . ix "bar") p2
Nothing
> preview (meta . ix "foo") p2
Just "bar"
También podemos generalizar a Lenses (realmente Traversals) que se dirigen a múltiples subpartes similares a la vez
dimensions :: Lens Point Double
> let p3 = over dimensions (+ 10) p0
> get latitude p3
10.0
> get longitude p3
10.0
> toListOf dimensions p3
[10.0, 10.0]
O incluso apuntar a subpartes simuladas que en realidad no existen pero aún forman una descripción equivalente de nuestros datos
eulerAnglePhi :: Lens Point Double
eulerAngleTheta :: Lens Point Double
eulerAnglePsi :: Lens Point Double
En términos generales, los lentes generalizan el tipo de interacción basada en la trayectoria entre valores enteros y subpartes de valores que la familia de funciones Clojure *-in
. Puede hacer mucho más en Haskell porque Haskell tiene una noción mucho más desarrollada de tipos y lentes, ya que los objetos de primera clase generalizan ampliamente las nociones de obtención y configuración que simplemente se presentan con las funciones *-in
.
assoc-in
puede ser más versátil que la lens
en algunos casos, porque puede crear niveles en la estructura si no existen.
lens
ofrece Folds
, que derriban la estructura y devuelven un resumen de los valores contenidos, y Traversals
que modifican los elementos de la estructura (posiblemente apuntan a varios elementos a la vez, posiblemente no hacen nada si los elementos objetivo no están presentes) mientras se mantiene La "forma" general de la estructura. Pero creo que sería difícil crear niveles intermedios utilizando lens
.
Otra diferencia que veo con las assoc-in
en Clojure es que parece que solo tienen que ver con la obtención y configuración de valores, mientras que la definición misma de una lente es compatible con "hacer algo con el valor", que algo posiblemente involucre a los lados. efectos
Por ejemplo, supongamos que tenemos una tupla (1,Right "ab")
. El segundo componente es un tipo de suma que puede contener una cadena. Queremos cambiar el primer carácter de la cadena leyéndolo desde la consola. Esto se puede hacer con lentes de la siguiente manera:
(_2._Right._Cons._1) (/_ -> getChar) (1,Right "ab")
-- reads char from console and returns the updated structure
Si la cadena no está presente, o está vacía, no se hace nada:
(_2._Right._Cons._1) (/_ -> getChar) (1,Left 5)
-- nothing read
(_2._Right._Cons._1) (/_ -> getChar) (1,Right "")
-- nothing read