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 .