web-dev-qa-db-fra.com

Que sont les prismes?

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:

  1. Déterminer si une entité appartient à une branche particulière d'un type de somme, et si c'est le cas, capturer les données sous-jacentes dans un tuple ou un singleton.
  2. Déstructurer et reconstruire une entité, éventuellement la modifier en cours.

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.

32
Michail

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'obtenir Nothing si le champ en question n'appartient pas à la branche que l'entité représente

L'utilisation de ^? Avec un vrai Lens ne renverra jamais Nothing, car un Lens s a Identifie exactement un a dans un s. Face à un champ d'enregistrement partiel,

data Wibble = Wobble { _wobble :: Int } | Wubble { _wubble :: Bool }

makeLenses générera un Traversal, pas un Lens.

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.

31
Benjamin Hodgson

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 Prisml 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"
14
phadej

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).

  • Il y a quelques 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
10
duplode