web-dev-qa-db-fra.com

Quels sont quelques exemples motivants pour Cofree CoMonad à Haskell?

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]
37
Josh.F

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 duplicatedé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 moves. 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)
56
pigworker