34 votes

A quoi servent les lentilles ?

Je n'arrive pas à trouver d'explication sur l'utilisation des lentilles dans des exemples pratiques. Ce court paragraphe de la page Hackage est le plus proche que j'ai trouvé :

Ce module offre un moyen pratique d'accéder aux éléments d'une structure et de les mettre à jour. Il est très similaire à Data.Accessors, mais un peu plus générique et a moins de dépendances. J'aime particulièrement la façon dont il gère proprement les structures imbriquées dans les monades d'état.

Alors, à quoi servent-ils ? Quels avantages et inconvénients présentent-ils par rapport aux autres méthodes ? Pourquoi sont-ils nécessaires ?

48voto

dflemstr Points 18999

Ils offrent une abstraction propre sur les mises à jour de données, et ne sont jamais vraiment "nécessaires". Ils vous permettent simplement de raisonner sur un problème d'une manière différente.

Dans certains langages de programmation impératifs/"orientés objet" comme le C, vous avez le concept familier d'une collection de valeurs (appelons-les "structs") et des moyens d'étiqueter chaque valeur de la collection (les étiquettes sont généralement appelées "champs"). Cela conduit à une définition comme celle-ci :

typedef struct { /* defining a new struct type */
  float x; /* field */
  float y; /* field */
} Vec2;

typedef struct {
  Vec2 col1; /* nested structs */
  Vec2 col2;
} Mat2;

Vous pouvez ensuite créer des valeurs de ce type nouvellement défini comme suit :

Vec2 vec = { 2.0f, 3.0f };
/* Reading the components of vec */
float foo = vec.x;
/* Writing to the components of vec */
vec.y = foo;

Mat2 mat = { vec, vec };
/* Changing a nested field in the matrix */
mat.col2.x = 4.0f;

De même, en Haskell, nous avons des types de données :

data Vec2 =
  Vec2
  { vecX :: Float
  , vecY :: Float
  }

data Mat2 =
  Mat2
  { matCol1 :: Vec2
  , matCol2 :: Vec2
  }

Ce type de données est ensuite utilisé comme suit :

let vec  = Vec2 2 3
    -- Reading the components of vec
    foo  = vecX vec
    -- Creating a new vector with some component changed.
    vec2 = vec { vecY = foo }

    mat = Mat2 vec2 vec2

Cependant, en Haskell, il n'existe pas de moyen simple de modifier les champs imbriqués dans une structure de données. En effet, vous devez recréer tous les objets enveloppants autour de la valeur que vous modifiez, car les valeurs Haskell sont immuables. Si vous avez une matrice comme celle ci-dessus en Haskell, et que vous voulez modifier la cellule supérieure droite de la matrice, vous devez écrire ceci :

    mat2 = mat { matCol2 = (matCol2 mat) { vecX = 4 } }

Ça marche, mais ça a l'air maladroit. Donc, ce que quelqu'un a trouvé, c'est essentiellement ça : Si vous regroupez deux choses : le "getter" d'une valeur (par exemple vecX y matCol2 ci-dessus) avec une fonction correspondante qui, étant donné la structure de données à laquelle le getter appartient, peut créer une nouvelle structure de données avec cette valeur modifiée, vous êtes en mesure de faire beaucoup de choses intéressantes. Par exemple :

data Data = Data { member :: Int }

-- The "getter" of the member variable
getMember :: Data -> Int
getMember d = member d

-- The "setter" or more accurately "updater" of the member variable
setMember :: Data -> Int -> Data
setMember d m = d { member = m }

memberLens :: (Data -> Int, Data -> Int -> Data)
memberLens = (getMember, setMember)

Il existe de nombreuses façons d'implémenter les lentilles ; pour ce texte, disons qu'une lentille est comme celle ci-dessus :

type Lens a b = (a -> b, a -> b -> a)

C'est-à-dire que c'est la combinaison d'un getter et d'un setter pour un type quelconque a qui a un champ de type b donc memberLens ci-dessus serait un Lens Data Int . Qu'est-ce que cela nous permet de faire ?

Commençons par créer deux fonctions simples qui extraient les getters et setters d'un objectif :

getL :: Lens a b -> a -> b
getL (getter, setter) = getter

setL :: Lens a b -> a -> b -> a
setL (getter, setter) = setter

Maintenant, nous pouvons commencer à faire abstraction des choses. Reprenons la situation ci-dessus, à savoir que nous voulons modifier une valeur "à deux étages". Nous ajoutons une structure de données avec un autre objectif :

data Foo = Foo { subData :: Data }

subDataLens :: Lens Foo Data
subDataLens = (subData, \ f s -> f { subData = s }) -- short lens definition

Maintenant, ajoutons une fonction qui compose deux objectifs :

(#) :: Lens a b -> Lens b c -> Lens a c
(#) (getter1, setter1) (getter2, setter2) =
    (getter2 . getter1, combinedSetter)
    where
      combinedSetter a x =
        let oldInner = getter1 a
            newInner = setter2 oldInner x
        in setter1 a newInner

Le code est écrit assez rapidement, mais je pense que ce qu'il fait est clair : les getters sont simplement composés ; vous obtenez la valeur de la donnée interne, puis vous lisez son champ. Le setter, lorsqu'il est censé modifier une certaine valeur a avec la nouvelle valeur du champ intérieur de x Dans un premier temps, le système récupère l'ancienne structure de données interne, définit son champ interne, puis met à jour la structure de données externe avec la nouvelle structure de données interne.

Maintenant, créons une fonction qui incrémente simplement la valeur d'un objectif :

increment :: Lens a Int -> a -> a
increment l a = setL l a (getL l a + 1)

Si nous avons ce code, ce qu'il fait devient clair :

d = Data 3
print $ increment memberLens d -- Prints "Data 4", the inner field is updated.

Maintenant, comme nous pouvons composer des objectifs, nous pouvons aussi le faire :

f = Foo (Data 5)
print $ increment (subDataLens#memberLens) f
-- Prints "Foo (Data 6)", the innermost field is updated.

Ce que font tous les paquets de lentilles, c'est essentiellement envelopper ce concept de lentilles - le regroupement d'un "setter" et d'un "getter" - dans un paquet soigné qui les rend faciles à utiliser. Dans une implémentation particulière de lentille, on pourrait écrire :

with (Foo (Data 5)) $ do
  subDataLens . memberLens $= 7

On se rapproche ainsi de la version C du code ; il devient très facile de modifier des valeurs imbriquées dans un arbre de structures de données.

Les lentilles ne sont rien d'autre que cela : un moyen facile de modifier des parties de certaines données. Parce qu'elles facilitent le raisonnement sur certains concepts, elles sont largement utilisées dans les situations où l'on dispose d'énormes ensembles de structures de données qui doivent interagir les unes avec les autres de diverses manières.

Pour les avantages et les inconvénients des lentilles, voir une question récente ici sur SO .

13voto

Don Stewart Points 94361

Les lentilles offrent des moyens pratiques de modifier structures de données, d'une manière uniforme et compositionnelle.

De nombreux programmes sont construits autour des opérations suivantes :

  • visualisation d'un composant d'une structure de données (éventuellement imbriquée)
  • mise à jour des champs des structures de données (éventuellement imbriquées)

Les licences fournissent un support linguistique pour l'affichage et l'édition des structures de manière à garantir la cohérence des éditions, à faciliter la composition des éditions et à utiliser le même code pour l'affichage des parties d'une structure et pour la mise à jour des parties de la structure.

Les lentilles permettent donc d'écrire facilement des programmes à partir de vues sur des structures, et à partir de structures sur des vues (et des éditeurs) pour ces structures. Elles nettoient une grande partie du désordre des accesseurs et des régleurs d'enregistrements.

Pierce et al. ont popularisé les lentilles, par exemple dans leur Papier Quotient Lenses et des implémentations pour Haskell sont maintenant largement utilisées (par exemple, le système de gestion de l'information). fclabels et les accesseurs de données).

Pour des cas d'utilisation concrets, envisagez :

  • les interfaces utilisateur graphiques, où l'utilisateur modifie les informations de manière structurée
  • analyseurs et jolies imprimantes
  • compilateurs
  • synchronisation de la mise à jour des structures de données
  • bases de données et schémas

et bien d'autres situations où vous disposez d'un modèle de structure de données du monde, et d'une vue modifiable sur ces données.

6voto

ertes Points 41

En outre, on oublie souvent que les objectifs mettent en œuvre une notion très générique d'"accès au champ et de mise à jour". Les lentilles peuvent être écrites pour toutes sortes de choses, y compris des objets de type fonction. Il faut un peu de réflexion abstraite pour comprendre cela, alors laissez-moi vous montrer un exemple de la puissance des lentilles :

at :: (Eq a) => a -> Lens (a -> b) b

Utilisation de at vous pouvez en fait accéder et manipuler des fonctions à arguments multiples en fonction des arguments précédents. Gardez simplement à l'esprit que Lens est une catégorie. C'est un idiome très utile pour ajuster localement des fonctions ou d'autres choses.

Vous pouvez également accéder aux données par des propriétés ou des représentations alternatives :

polar :: (Floating a, RealFloat a) => Lens (Complex a) (a, a)
mag   :: (RealFloat a) => Lens (Complex a) a

Vous pouvez aller plus loin en écrivant des objectifs pour accéder aux bandes individuelles d'un signal transformé par Fourier et bien plus encore.

3voto

missingfaktor Points 44003

Ces notes de cours de Bryan O'Sullivan couvrent ce sujet de manière très détaillée.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X