J'ai joué avec Cofree
, et je n'arrive pas à le comprendre.
Par exemple, je veux jouer avec Cofree [] Num
dans ghci et ne peut pas obtenir d'exemples intéressants.
Par exemple, si je construis un type Cofree:
let a = 1 :< [2, 3]
J'attendrais extract a == 1
, mais à la place, j'obtiens cette erreur:
No instance for (Num (Cofree [] a0)) arising from a use of ‘it’
In the first argument of ‘print’, namely ‘it’
In a stmt of an interactive GHCi command: print it
Et un type de:
extract a :: (Num a, Num (Cofree [] a)) => a
Puis-je obtenir des exemples simples, même triviaux, pour savoir comment utiliser Cofree avec, disons, des foncteurs: []
, ou Maybe
, ou Either
, qui illustre
extract
extend
unwrap
duplicate
?Cross Posté: https://www.reddit.com/r/haskell/comments/4wlw70/what_are_some_motivating_examples_for_cofree/
EDIT: Guidé par le commentaire de David Young, voici de meilleurs exemples qui montrent où mes premières tentatives ont été mal orientées, mais j'aimerais quand même quelques exemples qui peuvent guider une intuition de Cofree:
> let a = 1 :< []
> extract a
1
> let b = 1 :< [(2 :< []), (3 :< [])]
> extract b
1
> unwrap b
[2 :< [],3 :< []]
> map extract $ unwrap b
[2,3]
Récapitulons simplement la définition du type de données Cofree
.
data Cofree f a = a :< f (Cofree f a)
C'est au moins suffisant pour diagnostiquer le problème avec l'exemple. Quand tu as écrit
1 :< [2, 3]
vous avez fait une petite erreur signalée de façon plus subtile que utile. Ici, f = []
Et a
est quelque chose de numérique, car 1 :: a
. En conséquence, vous avez besoin
[2, 3] :: [Cofree [] a]
et donc
2 :: Cofree [] a
qui pourrait être ok si Cofree [] a
étaient également et une instance de Num
. Votre définition acquiert ainsi une contrainte qui ne sera probablement pas satisfaite, et en effet, lorsque vous tilisez votre valeur, la tentative de satisfaire la contrainte échoue.
Réessayez avec
1 :< [2 :< [], 3 :< []]
et vous devriez avoir plus de chance.
Voyons maintenant ce que nous avons. Commencez par rester simple. Qu'est-ce que Cofree f ()
? Qu'est-ce que Cofree [] ()
en particulier? Ce dernier est isomorphe au point fixe de []
: Les structures arborescentes où chaque nœud est une liste de sous-arbres, également appelés "rosiers non étiquetés". Par exemple.,
() :< [ () :< [ () :< []
, () :< []
]
, () :< []
]
De même, Cofree Maybe ()
est plus ou moins le point fixe de Maybe
: une copie des nombres naturels, car Maybe
nous donne soit zéro soit une position dans laquelle brancher un sous-arbre .
zero :: Cofree Maybe ()
zero = () :< Nothing
succ :: Cofree Maybe () -> Cofree Maybe ()
succ n = () :< Just n
Un cas trivial important est Cofree (Const y) ()
, qui est une copie de y
. Le foncteur Const y
Donne non positions pour les sous-arbres.
pack :: y -> Cofree (Const y) ()
pack y = () :< Const y
Ensuite, occupons-nous de l'autre paramètre. Il vous indique le type d'étiquette que vous attachez à chaque nœud. Renommer les paramètres de manière plus suggestive
data Cofree nodeOf label = label :< nodeOf (Cofree nodeOf label)
Lorsque nous étiquetons l'exemple (Const y)
, Nous obtenons paires
pair :: x -> y -> Cofree (Const y) x
pair x y = x :< Const y
Lorsque nous attachons des étiquettes aux nœuds de nos nombres, nous obtenons non vide listes
one :: x -> Cofree Maybe x
one = x :< Nothing
cons :: x -> Cofree Maybe x -> Cofree Maybe x
cons x xs = x :< Just xs
Et pour les listes, nous obtenons étiquetés rosiers.
0 :< [ 1 :< [ 3 :< []
, 4 :< []
]
, 2 :< []
]
Ces structures sont toujours "non vides", car il y a au moins un nœud supérieur, même s'il n'a pas d'enfants, et ce nœud aura toujours une étiquette. L'opération extract
vous donne l'étiquette du nœud supérieur.
extract :: Cofree f a -> a
extract (a :< _) = a
Autrement dit, extract
jette le contexte de l'étiquette supérieure.
Maintenant, l'opération duplicate
décore chaque étiquette avec son propre contexte.
duplicate :: Cofree f a -> Cofree f (Cofree f a)
duplicate a :< fca = (a :< fca) :< fmap duplicate fca -- f's fmap
Nous pouvons obtenir une instance de Functor
pour Cofree f
En visitant l'arbre entier
fmap :: (a -> b) -> Cofree f a -> Cofree f b
fmap g (a :< fca) = g a :< fmap (fmap g) fca
-- ^^^^ ^^^^
-- f's fmap ||||
-- (Cofree f)'s fmap, used recursively
Ce n'est pas difficile de voir ça
fmap extract . duplicate = id
parce que duplicate
décore chaque nœud avec son contexte, alors fmap extract
jette la décoration.
Notez que fmap
ne regarde que les étiquettes de l'entrée pour calculer les étiquettes de la sortie. Supposons que nous voulions calculer les étiquettes de sortie en fonction de chaque étiquette d'entrée dans son contexte? Par exemple, étant donné un arbre non étiqueté, nous pourrions vouloir étiqueter chaque nœud avec la taille de son sous-arbre entier. Grâce à l'instance Foldable
pour Cofree f
, Nous devrions pouvoir compter les nœuds avec.
length :: Foldable f => Cofree f a -> Int
Donc ça signifie
fmap length . duplicate :: Cofree f a -> Cofree f Int
L'idée clé des comonades est qu'elles capturent des "choses avec un certain contexte" et qu'elles vous permettent d'appliquer des cartes dépendantes du contexte partout.
extend :: Comonad c => (c a -> b) -> c a -> c b
extend f = fmap f -- context-dependent map everywhere
. -- after
duplicate -- decorating everything with its context
Définir extend
de façon plus directe vous évite les problèmes de duplication (bien que cela revienne uniquement au partage).
extend :: (Cofree f a -> b) -> Cofree f a -> Cofree f b
extend g ca@(_ :< fca) = g ca :< fmap (extend g) fca
Et vous pouvez récupérer duplicate
en prenant
duplicate = extend id -- the output label is the input label in its context
De plus, si vous choisissez extract
comme chose à faire pour chaque étiquette en contexte, il vous suffit de remettre chaque étiquette d'où elle vient:
extend extract = id
Ces "opérations sur les étiquettes en contexte" sont appelées "flèches co-Kleisli",
g :: c a -> b
et le travail de extend
est d'interpréter une flèche co-Kleisli comme une fonction sur des structures entières. L'opération extract
est la flèche d'identité co-Kleisli, et elle est interprétée par extend
comme la fonction d'identité. Bien sûr, il y a une composition co-Kleisli
(=<=) :: Comonad c => (c s -> t) -> (c r -> s) -> (c r -> t)
(g =<= h) = g . extend h
et les lois communes garantissent que =<=
est associatif et absorbe extract
, nous donnant la catégorie co-Kleisli. De plus, nous avons
extend (g =<= h) = extend g . extend h
de sorte que extend
est un functor (au sens catégorique) de la catégorie co-Kleisli aux ensembles-et-fonctions. Ces lois ne sont pas difficiles à vérifier pour Cofree
, car elles découlent des lois Functor
pour la forme du nœud.
Maintenant, un moyen utile de voir une structure dans une comonad cofree est comme une sorte de "serveur de jeu". Une structure
a :< fca
représente l'état du jeu. Un coup dans le jeu consiste soit à "arrêter", auquel cas vous obtenez le a
, soit à "continuer", en choisissant un sous-arbre du fca
. Par exemple, considérez
Cofree ((->) move) prize
Un client pour ce serveur doit soit arrêter, soit continuer en donnant un move
: c'est un liste de move
s. Le jeu se déroule comme suit:
play :: [move] -> Cofree ((->) move) prize -> prize
play [] (prize :< _) = prize
play (m : ms) (_ :< f) = play ms (f m)
Peut-être un move
est un Char
et le prize
est le résultat de l'analyse de la séquence de caractères.
Si vous regardez assez fort, vous verrez que [move]
Est une version de Free ((,) move) ()
. Les monades gratuites représentent les stratégies des clients. Le foncteur ((,) move)
Équivaut à une interface de commande avec uniquement la commande "envoyer un move
". Le foncteur ((->) move)
Est la structure correspondante "répondre à l'envoi d'un move
".
Certains foncteurs peuvent être vus comme capturant une interface de commande; la monade gratuite pour un tel foncteur représente des programmes qui font des commandes; le foncteur aura un "dual" qui représente comment répondre aux commandes; le comonad cofree du dual est la notion générale d'environnement dans lequel les programmes qui font des commandes peuvent être exécutés, avec l'étiquette indiquant quoi faire si le programme s'arrête et retourne une valeur, et les sous-structures indiquant comment continuer à exécuter le programme si il émet une commande.
Par exemple,
data Comms x = Send Char x | Receive (Char -> x)
décrit le droit d'envoyer ou de recevoir des caractères. Son double est
data Responder x = Resp {ifSend :: Char -> x, ifReceive :: (Char, x)}
Comme exercice, voyez si vous pouvez implémenter l'interaction
chatter :: Free Comms x -> Cofree Responder y -> (x, y)