Haskell Language
Lentille
Recherche…
Introduction
Lens est une bibliothèque pour Haskell qui fournit des objectifs, des isomorphismes, des plis, des traversées, des getters et des setters, exposant une interface uniforme pour interroger et manipuler des structures arbitraires, contrairement aux concepts d’accesseur et de mutateur de Java.
Remarques
Qu'est-ce qu'un objectif?
Les objectifs (et autres optiques) nous permettent de séparer la description de la manière dont nous voulons accéder à certaines données de ce que nous voulons en faire. Il est important de faire la distinction entre la notion abstraite d’une lentille et la mise en œuvre concrète. Comprendre de manière abstraite facilite beaucoup la programmation avec des lens
à long terme. Il existe de nombreuses représentations isomorphes des lentilles. Par conséquent, pour cette discussion, nous éviterons toute discussion concrète sur la mise en œuvre et donnerons une vue d'ensemble de haut niveau des concepts.
Mise au point
Un concept important dans la compréhension abstraite est la notion de focalisation . Les optiques importantes se concentrent sur une partie spécifique d'une structure de données plus grande sans oublier le contexte plus large. Par exemple, l'objectif _1
se concentre sur le premier élément d'un tuple mais n'oublie pas ce qui se trouvait dans le second champ.
Une fois que nous avons le focus, nous pouvons alors parler des opérations que nous sommes autorisés à effectuer avec un objectif. Étant donné un Lens sa
qui , lorsqu'il est donné un type de données de type s
se concentre sur un particulier a
, nous pouvons soit
- Extraire le
a
en oubliant le contexte supplémentaire ou - Remplace le
a
en fournissant une nouvelle valeur
Celles-ci correspondent aux opérations get
et set
bien connues qui sont généralement utilisées pour caractériser une lentille.
Autres optiques
Nous pouvons parler d'autres optiques d'une manière similaire.
Optique | Met l'accent sur... |
---|---|
Lentille | Une partie d'un produit |
Prisme | Une partie d'une somme |
Traversal | Zéro ou plusieurs parties d'une structure de données |
Isomorphisme | ... |
Chaque optique se concentre différemment, en tant que tel, selon le type d’optique, nous pouvons effectuer différentes opérations.
Composition
De plus, nous pouvons composer n'importe laquelle des deux optiques que nous avons déjà discutées afin de spécifier des accès de données complexes. Les quatre types d'optiques dont nous avons discuté forment un réseau, le résultat de la composition de deux optiques ensemble est leur limite supérieure.
Par exemple, si nous composons ensemble une lentille et un prisme, nous obtenons une traversée. La raison en est que, par leur composition (verticale), nous nous concentrons d'abord sur une partie d'un produit, puis sur une partie d'une somme. Le résultat étant une optique qui se concentre précisément sur zéro ou une partie de nos données, ce qui est un cas particulier d'une traversée. (Ceci est aussi parfois appelé une traversée affine).
À Haskell
La popularité de Haskell s'explique par la représentation très succincte de l'optique. Toutes les optiques ne sont que des fonctions d'une certaine forme qui peuvent être composées ensemble en utilisant la composition de fonctions. Cela conduit à une intégration très légère, ce qui facilite l'intégration de l'optique dans vos programmes. De plus, en raison des particularités de l'encodage, la composition de la fonction calcule automatiquement la limite supérieure de deux optiques que nous composons. Cela signifie que nous pouvons réutiliser les mêmes combinateurs pour différentes optiques sans coulée explicite.
Manipulation de tuples avec lentille
Obtenir
("a", 1) ^. _1 -- returns "a"
("a", 1) ^. _2 -- returns 1
Réglage
("a", 1) & _1 .~ "b" -- returns ("b", 1)
Modifier
("a", 1) & _2 %~ (+1) -- returns ("a", 2)
both
Traversal
(1, 2) & both *~ 2 -- returns (2, 4)
Objectifs pour disques
Enregistrement simple
{-# LANGUAGE TemplateHaskell #-}
import Control.Lens
data Point = Point {
_x :: Float,
_y :: Float
}
makeLenses ''Point
Les objectifs x
et y
sont créés.
let p = Point 5.0 6.0
p ^. x -- returns 5.0
set x 10 p -- returns Point { _x = 10.0, _y = 6.0 }
p & x +~ 1 -- returns Point { _x = 6.0, _y = 6.0 }
Gestion des enregistrements avec des noms de champs répétés
data Person = Person { _personName :: String }
makeFields ''Person
Crée une classe de type HasName
, le name
objectif pour Person
et fait de Person
une instance de HasName
. Les enregistrements suivants seront également ajoutés à la classe:
data Entity = Entity { _entityName :: String }
makeFields ''Entity
L'extension Template Haskell est requise pour que makeFields
fonctionne. Techniquement, il est tout à fait possible de créer les objectifs de cette manière par d'autres moyens, par exemple à la main.
Verres Stateful
Les opérateurs de lentilles ont des variantes utiles qui fonctionnent dans des contextes avec état. Ils sont obtenus en remplaçant ~
avec =
dans le nom de l'opérateur.
(+~) :: Num a => ASetter s t a a -> a -> s -> t
(+=) :: (MonadState s m, Num a) => ASetter' s a -> a -> m ()
Remarque: les variantes avec état ne sont pas censées modifier le type, elles ont donc les signatures d'
Lens'
ou d'Simple Lens'
.
Se débarrasser de &
chaînes
Si les opérations d'objectif doivent être enchaînées, cela ressemble souvent à ceci:
change :: A -> A
change a = a & lensA %~ operationA
& lensB %~ operationB
& lensC %~ operationC
Cela fonctionne grâce à l'associativité de &
. La version avec état est plus claire, cependant.
change a = flip execState a $ do
lensA %= operationA
lensB %= operationB
lensC %= operationC
Si lensX
est réellement id
, l'opération entière peut bien sûr être exécutée directement en la soulevant simplement avec la modify
.
Code impératif avec état structuré
En supposant que cet exemple indique:
data Point = Point { _x :: Float, _y :: Float }
data Entity = Entity { _position :: Point, _direction :: Float }
data World = World { _entities :: [Entity] }
makeLenses ''Point
makeLenses ''Entity
makeLenses ''World
Nous pouvons écrire du code qui ressemble aux langages impératifs classiques, tout en nous permettant d'utiliser les avantages de Haskell:
updateWorld :: MonadState World m => m ()
updateWorld = do
-- move the first entity
entities . ix 0 . position . x += 1
-- do some operation on all of them
entities . traversed . position %= \p -> p `pointAdd` ...
-- or only on a subset
entities . traversed . filtered (\e -> e ^. position.x > 100) %= ...
Écrire une lentille sans gabarit Haskell
Pour démystifier Template Haskell, supposez que vous avez
data Example a = Example { _foo :: Int, _bar :: a }
puis
makeLenses 'Example
produit (plus ou moins)
foo :: Lens' (Example a) Int bar :: Lens (Example a) (Example b) a b
Il n'y a rien de particulièrement magique, cependant. Vous pouvez les écrire vous-même:
foo :: Lens' (Example a) Int -- :: Functor f => (Int -> f Int) -> (Example a -> f (Example a)) ;; expand the alias foo wrap (Example foo bar) = fmap (\newFoo -> Example newFoo bar) (wrap foo) bar :: Lens (Example a) (Example b) a b -- :: Functor f => (a -> f b) -> (Example a -> f (Example b)) ;; expand the alias bar wrap (Example foo bar) = fmap (\newBar -> Example foo newBar) (wrap bar)
Essentiellement, vous voulez "visiter" le "focus" de votre objectif avec la fonction wrap
, puis reconstruire le type "entier".
Lens et Prism
Une Lens' sa
signifie que vous pouvez toujours trouver un a
dans n'importe quel s
. Un Prism' sa
signifie que vous pouvez parfois trouver que s
est juste a
mais parfois c'est autre chose.
Pour être plus clair, nous avons _1 :: Lens' (a, b) a
parce que tout tuple a toujours un premier élément. Nous avons _Just :: Prism' (Maybe a) a
parce que parfois Maybe a
est a
valeur enveloppée dans Just
mais parfois c'est Nothing
.
Avec cette intuition, certains combinateurs standard peuvent être interprétés parallèlement les uns aux autres
-
view :: Lens' sa -> (s -> a)
"obtient" lea
des
-
set :: Lens' sa -> (a -> s -> s)
"définit"a
emplacement danss
-
review :: Prism' sa -> (a -> s)
"réalise" qu'una
pourrait être uns
-
preview :: Prism' sa -> (s -> Maybe a)
"tente" de transformer uns
ena
.
Une autre façon d'y penser est qu'une valeur de type Lens' sa
démontre que s
a la même structure que (r, a)
pour un r
inconnu. Par contre, Prism' sa
démontre que s
a la même structure que r
Either ra
pour certains r
. Nous pouvons écrire ces quatre fonctions ci-dessus avec cette connaissance:
-- `Lens' s a` is no longer supplied, instead we just *know* that `s ~ (r, a)` view :: (r, a) -> a view (r, a) = a set :: a -> (r, a) -> (r, a) set a (r, _) = (r, a) -- `Prism' s a` is no longer supplied, instead we just *know* that `s ~ Either r a` review :: a -> Either r a review a = Right a preview :: Either r a -> Maybe a preview (Left _) = Nothing preview (Right a) = Just a
Les traversées
Un Traversal' sa
montre que s
a 0 à beaucoup a
s à l' intérieur de celui - ci.
toListOf :: Traversal' s a -> (s -> [a])
Tout type t
qui est Traversable
automatiquement ce traverse :: Traversal (ta) a
.
Nous pouvons utiliser un Traversal
pour définir ou carte sur tous ces a
valeur
> set traverse 1 [1..10]
[1,1,1,1,1,1,1,1,1,1]
> over traverse (+1) [1..10]
[2,3,4,5,6,7,8,9,10,11]
Un f :: Lens' sa
dit qu'il y a exactement a
intérieur de s
. A g :: Prism' ab
dit qu'il y a 0 ou 1 b
s dans a
. Composer f . g
nous donne un Traversal' sb
car suite à f
puis g
montre comment il y a 0 à 1 b
s dans s
.
Les lentilles composent
Si vous avez un f :: Lens' ab
et un g :: Lens' bc
alors f . g
est une Lens' ac
obtenue en suivant f
abord et ensuite g
. Notamment:
- Les lentilles composent des fonctions (ce sont vraiment des fonctions)
- Si vous pensez à la fonctionnalité de
view
deLens
, il semble que les flux de données "de gauche à droite" - cela peut sembler en arrière à votre intuition normale pour la composition de la fonction. Par contre, vous devriez vous sentir naturel si vous pensez.
-notation comme cela se passe dans les langages OO.
Plus que la simple composition de l’ Lens
avec l’ Lens
, (.)
Peut être utilisé pour composer presque tous les types de type " Lens
". Il n'est pas toujours facile de voir le résultat puisque le type devient plus difficile à suivre, mais vous pouvez utiliser le tableau des lens
pour le comprendre. La composition x . y
a le type de la limite inférieure des types de x
et y
dans ce graphique.
Lentilles de classe
Outre la fonction standard makeLenses
pour générer des Lens
, Control.Lens.TH
offre également la fonction makeClassy
. makeClassy
a le même type et fonctionne de la même manière que makeLenses
, avec une différence essentielle. En plus de générer les objectifs standard et les traversées, si le type n'a pas d'arguments, il créera également une classe décrivant tous les types de données qui possèdent le type en tant que champ. Par exemple
data Foo = Foo { _fooX, _fooY :: Int }
makeClassy ''Foo
créera
class HasFoo t where
foo :: Simple Lens t Foo
instance HasFoo Foo where foo = id
fooX, fooY :: HasFoo t => Simple Lens t Int
Champs avec makeFields
(Cet exemple copié à partir de cette réponse StackOverflow )
Disons que vous avez un certain nombre de types de données différents que tous devraient avoir une lentille du même nom, en l'occurrence la capacity
. La tranche makeFields
va créer une classe qui accomplit cela sans conflits d'espace de noms.
{-# LANGUAGE FunctionalDependencies
, MultiParamTypeClasses
, TemplateHaskell
#-}
module Foo
where
import Control.Lens
data Foo
= Foo { fooCapacity :: Int }
deriving (Eq, Show)
$(makeFields ''Foo)
data Bar
= Bar { barCapacity :: Double }
deriving (Eq, Show)
$(makeFields ''Bar)
Puis en ghci:
*Foo
λ let f = Foo 3
| b = Bar 7
|
b :: Bar
f :: Foo
*Foo
λ fooCapacity f
3
it :: Int
*Foo
λ barCapacity b
7.0
it :: Double
*Foo
λ f ^. capacity
3
it :: Int
*Foo
λ b ^. capacity
7.0
it :: Double
λ :info HasCapacity
class HasCapacity s a | s -> a where
capacity :: Lens' s a
-- Defined at Foo.hs:14:3
instance HasCapacity Foo Int -- Defined at Foo.hs:14:3
instance HasCapacity Bar Double -- Defined at Foo.hs:19:3
Donc, ce qui est réellement fait est déclaré une classe HasCapacity sa
, où la capacité est une Lens'
de s
à a
( a
est fixé une fois que s est connu). Il a compris le nom "capacity" en supprimant le nom (en minuscules) du type de données du champ; Je trouve agréable de ne pas avoir à utiliser un trait de soulignement sur le nom du champ ou sur le nom de l'objectif, car parfois la syntaxe d'enregistrement est ce que vous voulez. Vous pouvez utiliser makeFieldsWith et les diverses lentilles pour avoir différentes options pour calculer les noms d'objectif.
Au cas où cela vous aiderait, utilisez ghci -ddump-splices Foo.hs:
[1 of 1] Compiling Foo ( Foo.hs, interpreted )
Foo.hs:14:3-18: Splicing declarations
makeFields ''Foo
======>
class HasCapacity s a | s -> a where
capacity :: Lens' s a
instance HasCapacity Foo Int where
{-# INLINE capacity #-}
capacity = iso (\ (Foo x_a7fG) -> x_a7fG) Foo
Foo.hs:19:3-18: Splicing declarations
makeFields ''Bar
======>
instance HasCapacity Bar Double where
{-# INLINE capacity #-}
capacity = iso (\ (Bar x_a7ne) -> x_a7ne) Bar
Ok, modules loaded: Foo.
La première épissure a donc fait de la classe HasCapcity
et ajouté une instance pour Foo; le second utilisait la classe existante et créait une instance pour Bar.
Cela fonctionne également si vous importez la classe HasCapcity
d'un autre module; makeFields
peut ajouter plus d'instances à la classe existante et répartir vos types sur plusieurs modules. Mais si vous l'utilisez à nouveau dans un autre module où vous n'avez pas importé la classe, cela créera une nouvelle classe (avec le même nom) et vous aurez deux objectifs de capacité surchargés distincts qui ne sont pas compatibles.