lentes, fclabels, data-accessor – qué biblioteca para el acceso a la estructura y la mutación es mejor

Hay al menos tres bibliotecas populares para acceder y manipular campos de registros. Los que conozco son: datos-accessor, fclabels y lentes.

Personalmente comencé con el acceso a datos y los estoy usando ahora. Sin embargo, recientemente en Haskell-Cafe hubo una opinión de que fclabels era superior.

Por lo tanto, estoy interesado en comparar esas tres (y quizás más) bibliotecas.

Hay al menos 4 bibliotecas que conozco que proporcionan lentes.

La noción de una lente es que proporciona algo isomorfo a

 data Lens ab = Lens (a -> b) (b -> a -> a) 

proporcionando dos funciones: un getter y un setter

 get (Lens g _) = g put (Lens _ s) = s 

sujeto a tres leyes:

Primero, si pones algo, puedes sacarlo de nuevo

 get l (put lba) = b 

Segundo, que obtener y luego establecer no cambia la respuesta

 put l (get la) a = a 

Y tercero, poner dos veces es lo mismo que poner una vez, o mejor dicho, que la segunda jugada gana.

 put l b1 (put l b2 a) = put l b1 a 

Tenga en cuenta que el sistema de tipo no es suficiente para verificar estas leyes por usted, por lo que debe asegurarse de hacerlo usted mismo sin importar la implementación de lentes que use.

Muchas de estas bibliotecas también ofrecen un conjunto de combinadores adicionales en la parte superior y, por lo general, algún tipo de plantilla de maquinaria para generar automáticamente lentes para los campos de tipos de registros simples.

Con eso en mente, podemos recurrir a las diferentes implementaciones:

Implementaciones

fclabels

fclabels es tal vez la explicación más fácil de las bibliotecas de lentes, porque es a :-> b se puede traducir directamente al tipo anterior. Proporciona una instancia de Categoría para (:->) que es útil, ya que le permite componer lentes. También proporciona un tipo de Point sin ley que generaliza la noción de una lente utilizada aquí, y algo de fontanería para tratar con isomorfismos.

Un obstáculo para la adopción de fclabels es que el paquete principal incluye la plantilla de plomería de Haskell, por lo que el paquete no es Haskell 98, y también requiere la extensión TypeOperators (bastante controvertida).

acceso a datos

[Editar: data-accessor ya no está utilizando esta representación, pero se ha movido a una forma similar a la de data-lens de data-lens . Me quedo con este comentario, sin embargo]

data-accessor es algo más popular que fclabels , en parte porque es Haskell 98. Sin embargo, su elección de representación interna me hace vomitar en mi boca un poco.

El tipo T que utiliza para representar una lente se define internamente como

 newtype T ra = Cons { decons :: a -> r -> (a, r) } 

En consecuencia, para get el valor de una lente, debe enviar un valor indefinido para el argumento ‘a’. Esto me parece una implementación increíblemente fea y ad hoc.

Dicho esto, Henning ha incluido la plomería template-haskell para generar automáticamente los accessors en un paquete separado ‘ data-accessor-template ‘.

Tiene el beneficio de un conjunto decentemente grande de paquetes que ya lo emplean, siendo Haskell 98, y proporciona la instancia de Category importante, por lo que si no se presta atención a cómo se hace la salchicha, este paquete es realmente bastante razonable elección.

lentes

A continuación, está el paquete de lentes , que observa que una lente puede proporcionar un homomorfismo de mónada de estado entre dos mónadas de estado, definiendo las lentes directamente como tales homomorfismos de mónada.

Si realmente se molestara en proporcionar un tipo para sus lentes, tendrían un tipo de rango 2 como:

 newtype Lens st = Lens (forall a. State ta -> State sa) 

Como resultado, no me gusta este enfoque, ya que innecesariamente te saca de Haskell 98 (si quieres que un tipo lo proporcione a tus lentes en abstracto) y te priva de la instancia de Category para lentes, lo que te dejaría los compones con . . La implementación también requiere clases de tipo multiparámetro.

Tenga en cuenta que todas las otras bibliotecas de lentes mencionadas aquí proporcionan algún combinador o se pueden usar para proporcionar el mismo efecto de focalización de estado, de modo que no se gana nada codificando su lente directamente de esta manera.

Además, las condiciones secundarias indicadas al principio no tienen realmente una buena expresión en esta forma. Al igual que con ‘fclabels’, esto proporciona el método template-haskell para generar automáticamente lentes para un tipo de registro directamente en el paquete principal.

Debido a la falta de instancia de Category , la encoding barroca y el requisito de template-haskell en el paquete principal, esta es la implementación menos preferida.

lente de datos

[Editar: A partir de 1.8.0, estos han pasado del paquete comonad-transformers al lente de datos]

Mi paquete de lentes de data-lens proporciona lentes en términos de la comonad de la tienda .

 newtype Lens ab = Lens (a -> Store ba) 

dónde

 data Store ba = Store (b -> a) b 

Ampliado esto es equivalente a

 newtype Lens ab = Lens (a -> (b, b -> a)) 

Puedes ver esto como factorizar el argumento común del getter y el setter para devolver un par que consiste en el resultado de recuperar el elemento, y un setter para poner un nuevo valor nuevamente. Esto ofrece el beneficio computacional que el ‘setter’ aquí puede reciclar parte del trabajo utilizado para obtener el valor, lo que permite una operación de ‘modificación’ más eficiente que en la definición de fclabels , especialmente cuando los accessors están encadenados.

También hay una buena justificación teórica para esta representación, porque el subconjunto de valores de “Lente” que satisfacen las 3 leyes establecidas al principio de esta respuesta son precisamente aquellos lentes para los que la función envuelta es una “coalgebra comonad” para la tienda comonad . Esto transforma 3 leyes peludas para una lente l hasta 2 equivalentes de Pointless aptos:

 extract . l = id duplicate . l = fmap l . l 

Este enfoque se notó por primera vez y se describe en Functor Russell O’Connor es Lens como Applicative es para Biplate : Introducción a Multiplate y se blogueó sobre la base de una preimpresión de Jeremy Gibbons.

También incluye una serie de combinadores para trabajar con lentes estrictamente y algunas lentes estándar para contenedores, como Data.Map .

Entonces las lentes en data-lens forman una Category (a diferencia del paquete de lenses ), son Haskell 98 (a diferencia de fclabels / fclabels ), son sanas (a diferencia del back-end del data-accessor ) y proporcionan una implementación un poco más eficiente, data-lens-fd proporciona la funcionalidad para trabajar con MonadState para aquellos dispuestos a salir de Haskell 98, y la maquinaria template-haskell ahora está disponible a través de data-lens-template .

Actualización 28/06/2012: Otras estrategias de implementación de lentes

Lentes de Isomorfismo

Hay otras dos codificaciones de lentes que vale la pena considerar. El primero ofrece una buena forma teórica de ver una lente como una forma de dividir una estructura en el valor del campo y “todo lo demás”.

Dado un tipo para isomorfismos

 data Iso ab = Iso { hither :: a -> b, yon :: b -> a } 

de tal manera que los miembros válidos satisfacen hither . yon = id hither . yon = id , y yon . hither = id yon . hither = id

Podemos representar una lente con:

 data Lens ab = forall c. Lens (Iso a (b,c)) 

Estos son principalmente útiles como una forma de pensar sobre el significado de los lentes, y podemos usarlos como una herramienta de razonamiento para explicar otros lentes.

Lentes Van Laarhoven

Podemos modelar lentes de modo que se puedan componer con (.) id , incluso sin una instancia de Category utilizando

 type Lens ab = forall f. Functor f => (b -> fb) -> a -> fa 

como el tipo de nuestras lentes.

Entonces, definir una lente es tan fácil como:

 _2 f (a,b) = (,) a <$> fb 

y puedes validar por ti mismo que la composición de la función es la composición de la lente.

Recientemente escribí sobre cómo puede generalizar aún más las lentes van Laarhoven para obtener familias de lentes que puedan cambiar los tipos de campos, simplemente generalizando esta firma para

 type LensFamily abcd = forall f. Functor f => (c -> fd) -> a -> fb 

Esto tiene la desafortunada consecuencia de que la mejor manera de hablar sobre lentes es usar el polymorphism de rango 2, pero no es necesario que use esa firma directamente al definir los lentes.

La Lens I que LensFamily definir para _2 es en realidad una LensFamily .

 _2 :: Functor f => (a -> fb) -> (c,a) -> f (c, b) 

He escrito una biblioteca que incluye lentes, familias de lentes y otras generalizaciones, como getters, setters, pliegues y recorridos. Está disponible en hackage como el paquete de lens .

Una vez más, una gran ventaja de este enfoque es que los mantenedores de la biblioteca pueden crear lentes con este estilo en sus bibliotecas sin incurrir en ninguna dependencia de biblioteca de lentes, simplemente suministrando funciones con el tipo Functor f => (b -> fb) -> a -> fa , para sus tipos particulares ‘a’ y ‘b’. Esto reduce en gran medida el costo de la adopción.

Como no es necesario que use realmente el paquete para definir nuevas lentes, se quita mucha presión de mis preocupaciones anteriores acerca de mantener la biblioteca Haskell 98.