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

  1. Extraire le a en oubliant le contexte supplémentaire ou
  2. 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.

entrer la description de l'image ici

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" le a de s
  • set :: Lens' sa -> (a -> s -> s) "définit" a emplacement dans s
  • review :: Prism' sa -> (a -> s) "réalise" qu'un a pourrait être un s
  • preview :: Prism' sa -> (s -> Maybe a) "tente" de transformer un s en a .

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 de Lens , 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.



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow