J'essaie de mieux comprendre la bibliothèque lens
, donc je joue avec les types qu'elle propose. J'ai déjà eu une certaine expérience avec les lentilles et je sais à quel point elles sont puissantes et pratiques. Je suis donc passé à Prismes, et je suis un peu perdu. Il semble que les prismes permettent deux choses:
Le premier point semble utile, mais généralement on n'a pas besoin de toutes les données d'une entité, et ^?
avec des lentilles simples permet d'obtenir Nothing
si le champ en question n'appartient pas à la branche que l'entité représente, tout comme avec les prismes.
Le deuxième point ... Je ne sais pas, pourrait avoir des utilisations?
La question est donc: que puis-je faire avec un prisme que je ne peux pas avec d'autres optiques?
Edit : merci à tous pour vos excellentes réponses et liens pour une lecture plus approfondie! J'aimerais pouvoir tous les accepter.
Les lentilles caractérisent la relation has-a; Les prismes caractérisent la relation is-a
Un Lens s a
Dit "s
a una
"; il a des méthodes pour obtenir exactement un a
à partir d'un s
et pour écraser exactement un a
dans un s
. Un Prism s a
Dit "a
est uns
"; il a des méthodes pour convertir un a
en un s
et (tenter de) convertir un s
en un a
.
Mettre cette intuition dans le code vous donne la formulation familière "get-set" (ou "costate comonad coalgebra") des lentilles,
data Lens s a = Lens {
get :: s -> a,
set :: a -> s -> s
}
et une représentation "upcast-downcast" des prismes,
data Prism s a = Prism {
up :: a -> s,
down :: s -> Maybe a
}
up
injecte un a
dans s
(sans ajouter aucune information), et down
teste si le s
est un a
.
Dans lens
, up
est orthographié review
et down
est preview
. Il n'y a pas de constructeur Prism
; vous utilisez le constructeur intelligent prism'
.
Que pouvez-vous faire avec un Prism
? Injecter et projeter des types de somme!
_Left :: Prism (Either a b) a
_Left = Prism {
up = Left,
down = either Just (const Nothing)
}
_Right :: Prism (Either a b) b
_Right = Prism {
up = Right,
down = either (const Nothing) Just
}
Les objectifs ne prennent pas en charge cela - vous ne pouvez pas écrire une Lens (Either a b) a
parce que vous ne pouvez pas implémenter get :: Either a b -> a
. En pratique, vous pouvez écrire une Traversal (Either a b) a
, mais cela ne vous permet pas de créer un Either a b
À partir d'un a
- il vous permettra seulement d'écraser un a
qui est déjà là.
Mis à part: Je pense que ce point subtil sur
Traversal
s est la source de votre confusion au sujet des champs d'enregistrement partiels.
^?
Avec des lentilles simples permet d'obtenirNothing
si le champ en question n'appartient pas à la branche que l'entité représenteL'utilisation de
^?
Avec un vraiLens
ne renverra jamaisNothing
, car unLens s a
Identifie exactement una
dans uns
. Face à un champ d'enregistrement partiel,data Wibble = Wobble { _wobble :: Int } | Wubble { _wubble :: Bool }
makeLenses
générera unTraversal
, pas unLens
.wobble :: Traversal' Wibble Int wubble :: Traversal' Wibble Bool
Pour un exemple de la façon dont Prism
peut être appliqué dans la pratique, regardez Control.Exception.Lens
, qui fournit une collection de Prism
dans l'extensible de Haskell Hiérarchie Exception
. Cela vous permet d'effectuer des tests de type d'exécution sur SomeException
et d'injecter des exceptions spécifiques dans SomeException
.
_ArithException :: Prism' SomeException ArithException
_AsyncException :: Prism' SomeException AsyncException
-- etc.
(Ce sont des versions légèrement simplifiées des types réels. En réalité, ces prismes sont des méthodes de classe surchargées.)
En pensant à un niveau supérieur, certains programmes entiers peuvent être considérés comme "fondamentalement un Prism
". L'encodage et le décodage des données en sont un exemple: vous pouvez toujours convertir des données structurées en un String
, mais tous les String
ne peuvent pas être analysés:
showRead :: (Show a, Read a) => Prism String a
showRead = Prism {
up = show,
down = listToMaybe . fmap fst . reads
}
Pour résumer, Lens
es et Prism
s codent ensemble les deux principaux outils de conception de la programmation, de la composition et du sous-typage orientés objet. Lens
es sont une version de première classe des opérateurs Java .
Et =
, Et Prism
sont une version de première classe des Java instanceof
et la conversion ascendante implicite.
Une façon fructueuse de penser aux Lens
es est de vous donner un moyen de diviser un composite s
en une valeur ciblée a
et un certain contexte c
. Pseudocode:
type Lens s a = exists c. s <-> (a, c)
Dans ce cadre, un Prism
vous donne un moyen de considérer un s
comme étant soit un a
soit un contexte c
.
type Prism s a = exists c. s <-> Either a c
(Je vous laisse le soin de vous convaincre qu'elles sont isomorphes aux représentations simples que j'ai montrées ci-dessus. Essayez d'implémenter get
/set
/up
/down
pour ces types!)
En ce sens, un Prism
est un co - Lens
. Either
est le dual catégorique de (,)
; Prism
est le double catégorique de Lens
.
Vous pouvez également observer cette dualité dans la formulation "optique proféteur" - Strong
et Choice
sont doubles .
type Lens s t a b = forall p. Strong p => p a b -> p s t
type Prism s t a b = forall p. Choice p => p a b -> p s t
C'est plus ou moins la représentation que lens
utilise, car ces Lens
es et Prism
sont très composables. Vous pouvez composer Prism
pour agrandir Prism
s ("a
est uns
, qui est unp
") en utilisant (.)
; composer un Prism
avec un Lens
vous donne un Traversal
.
Je viens d'écrire un article de blog, qui pourrait aider à construire une intuition sur les prismes: Les prismes sont des constructeurs (Les lentilles sont des champs). http://oleg.fi/gists/posts/2018-06-19-prisms-are-constructors.html
Les prismes pourraient être introduits comme correspondance de motifs de première classe, mais c'est une vue unilatérale. Je dirais que ce sont des constructeurs généralisés , bien qu'ils soient peut-être plus souvent utilisés pour la correspondance de motifs que pour la construction réelle.
La propriété importante des constructeurs (et prismes licites), est leur injectivité. Bien que les lois habituelles du prisme ne le disent pas directement, la propriété d'injectivité peut être déduite.
Pour citer lens
- documentation de bibliothèque, les lois des prismes sont:
Tout d'abord, si je review
une valeur avec Prism
puis preview
, je la récupérerai:
preview l (review l b) ≡ Just b
Deuxièmement, si vous pouvez extraire une valeur a à l'aide d'un Prism
l
d'une valeur s
, la valeur s
est complètement décrite par l
et a
:
preview l s ≡ Just a ⇒ review l a ≡ s
En fait, la première loi suffit à elle seule à prouver l'injectivité de la construction via Prism
:
review l x ≡ review l y ⇒ x ≡ y
La preuve est simple:
review l x ≡ review l y
-- x ≡ y -> f x ≡ f y
preview l (review l x) ≡ preview l (review l y)
-- rewrite both sides with the first law
Just x ≡ Just y
-- injectivity of Just
x ≡ y
Nous pouvons utiliser la propriété d'injectivité comme un outil supplémentaire dans la boîte à outils de raisonnement équationnel. Ou nous pouvons l'utiliser comme une propriété facile à vérifier pour décider si quelque chose est un Prism
légal. La vérification est facile car nous n'avons que le côté review
de Prism
. De nombreux constructeurs intelligents, qui par exemple normalisent les données d'entrée, ne sont pas des prismes légaux.
Un exemple utilisant case-insensitive
:
-- Bad!
_CI :: FoldCase s => Prism' (CI s) s
_CI = prism' ci (Just . foldedCase)
λ> review _CI "FOO" == review _CI "foo"
True
λ> "FOO" == "foo"
False
La première loi est également violée:
λ> preview _CI (review _CI "FOO")
Just "foo"
En plus des autres excellentes réponses, j'estime que Iso
offre un joli point de vue pour examiner cette question.
Il y a quelques i :: Iso' s a
signifie que si vous avez une valeur s
, vous avez également (virtuellement) une valeur a
, et vice versa. Le Iso'
vous offre deux fonctions de conversion, view i :: s -> a
et review i :: a -> s
qui sont à la fois garantis pour réussir et sans perte.
Il y a quelques l :: Lens' s a
signifie que si vous avez un s
, vous avez également un a
, mais pas l'inverse . view l :: s -> a
peut laisser tomber des informations en cours de route, car la conversion ne doit pas être sans perte, et vous ne pouvez donc pas aller dans l'autre sens si tout ce que vous avez est un a
(cf. set l :: a -> s -> s
, qui nécessite également un s
en plus de la valeur a
pour fournir les informations manquantes).
p :: Prism' s a
signifie que si vous avez une valeur s
, vous pourriez avoir également un a
, mais il n'y a aucune garantie. La conversion preview p :: s -> Maybe a
n'est pas garanti pour réussir. Pourtant, vous avez l'autre sens, review p :: a -> s
.En d'autres termes, un Iso
est inversible et réussit toujours. Si vous supprimez l'exigence d'invertibilité, vous obtenez un Lens
; si vous supprimez la garantie de succès, vous obtenez un Prism
. Si vous déposez les deux, vous obtenez un traversée affine (qui n'est pas dans l'objectif en tant que type séparé), et si vous aller plus loin et renoncer à avoir au plus une cible, vous vous retrouvez avec un Traversal
. Cela se reflète dans l'un des diamants de la hiérarchie des sous-types de lentilles :
Traversal
/ \
/ \
/ \
Lens Prism
\ /
\ /
\ /
Iso