Quelqu'un peut-il expliquer la fonction de traversée dans Haskell?

99

J'essaye et je ne parviens pas à faire traversefonctionner la fonction Data.Traversable. Je ne peux pas voir son point. Puisque je viens d'un milieu impératif, quelqu'un peut-il me l'expliquer en termes de boucle impérative? Un pseudo-code serait très apprécié. Merci.

Konan
la source
1
L'article L'essence du modèle d'itérateur pourrait être utile car il construit la notion de cheminement étape par étape. Quelques concepts avancés sont cependant présents
Jackie

Réponses:

121

traverseest identique à fmap, sauf qu'il vous permet également d'exécuter des effets pendant que vous reconstruisez la structure de données.

Jetez un œil à l'exemple de la Data.Traversabledocumentation.

 data Tree a = Empty | Leaf a | Node (Tree a) a (Tree a)

L' Functorinstance de Treeserait:

instance Functor Tree where
  fmap f Empty        = Empty
  fmap f (Leaf x)     = Leaf (f x)
  fmap f (Node l k r) = Node (fmap f l) (f k) (fmap f r)

Il reconstruit l'arborescence entière, en appliquant fà chaque valeur.

instance Traversable Tree where
    traverse f Empty        = pure Empty
    traverse f (Leaf x)     = Leaf <$> f x
    traverse f (Node l k r) = Node <$> traverse f l <*> f k <*> traverse f r

L' Traversableinstance est presque la même, sauf que les constructeurs sont appelés dans un style applicatif. Cela signifie que nous pouvons avoir des effets (secondaires) lors de la reconstruction de l'arbre. L'applicatif est presque le même que les monades, sauf que les effets ne peuvent pas dépendre des résultats précédents. Dans cet exemple, cela signifie que vous ne pouvez pas faire quelque chose de différent sur la branche droite d'un nœud en fonction des résultats de la reconstruction de la branche gauche par exemple.

Pour des raisons historiques, la Traversableclasse contient également une version monadique de traversecalled mapM. À toutes fins utiles, mapMc'est le même que traverse- il existe en tant que méthode distincte parce Applicativeque ce n'est que plus tard devenu une superclasse de Monad.

Si vous l'implémentiez dans un langage impur, ce fmapserait la même chose traverse, car il n'y a aucun moyen d'éviter les effets secondaires. Vous ne pouvez pas l'implémenter en boucle, car vous devez parcourir votre structure de données de manière récursive. Voici un petit exemple comment je le ferais en Javascript:

Node.prototype.traverse = function (f) {
  return new Node(this.l.traverse(f), f(this.k), this.r.traverse(f));
}

L'implémenter comme ça vous limite cependant aux effets que le langage permet. Si vous voulez le non-déterminisme (qui est l'instance de liste des modèles applicatifs) et que votre langage ne l'a pas intégré, vous n'avez pas de chance.

Sjoerd Visscher
la source
11
Que signifie le terme «effet»?
missingfaktor
24
@missingfaktor: Cela signifie les informations structurelles de a Functor, la partie qui n'est pas paramétrique. La valeur de l'état dans State, l'échec dans Maybeet Either, le nombre d'éléments dans [], et bien sûr les effets secondaires externes arbitraires dans IO. Je m'en fiche en tant que terme générique (comme les Monoidfonctions utilisant "vide" et "append", le concept est plus générique que ce que le terme suggère au début) mais c'est assez courant et sert assez bien le but.
CA McCann
@CA McCann: Compris. Merci de répondre!
missingfaktor
1
"Je suis presque sûr que vous n'êtes pas censé faire ça [...]." Certainement pas - ce serait aussi désagréable que de faire apdépendre les effets des résultats précédents. J'ai reformulé cette remarque en conséquence.
duplode
2
"L'applicatif est presque le même que les monades, sauf que les effets ne peuvent dépendre des résultats précédents." ... beaucoup de choses se sont mises en place pour moi avec cette ligne, merci!
Agam
58

traversetransforme les choses à l'intérieur de a Traversableen Traversabledes choses «à l'intérieur» de an Applicative, étant donné une fonction qui fait Applicativedes choses à partir des choses.

Utilisons Maybecomme Applicativeet listons comme Traversable. Nous avons d'abord besoin de la fonction de transformation:

half x = if even x then Just (x `div` 2) else Nothing

Donc, si un nombre est pair, nous en obtenons la moitié (à l'intérieur de a Just), sinon nous obtenons Nothing. Si tout se passe "bien", cela ressemble à ceci:

traverse half [2,4..10]
--Just [1,2,3,4,5]

Mais...

traverse half [1..10]
-- Nothing

La raison est que la <*>fonction est utilisée pour construire le résultat, et quand l'un des arguments est Nothing, nous Nothingrevenons.

Un autre exemple:

rep x = replicate x x

Cette fonction génère une liste de longueur xavec le contenu x, par exemple rep 3= [3,3,3]. Quel est le résultat traverse rep [1..3]?

Nous obtenons les résultats partiels [1], [2,2]et l' [3,3,3]utilisation rep. Maintenant , la sémantique des listes comme Applicativesest « prendre toutes les combinaisons », par exemple , (+) <$> [10,20] <*> [3,4]est [13,14,23,24].

"Toutes les combinaisons" de [1]et [2,2]sont deux fois [1,2]. Toutes les combinaisons de deux fois [1,2]et [3,3,3]six fois [1,2,3]. Donc nous avons:

traverse rep [1..3]
--[[1,2,3],[1,2,3],[1,2,3],[1,2,3],[1,2,3],[1,2,3]]
Landei
la source
1
Votre résultat final me rappelle cela .
hugomg
3
@missingno: Ouais, ils ont ratéfac n = length $ traverse rep [1..n]
Landei
1
En fait, c'est là sous "List-encoding-programmer" (mais en utilisant des list comprehensions). Ce site Web est complet :)
hugomg
1
@missingno: Hm, ce n'est pas exactement la même chose ... les deux s'appuient sur le comportement du produit cartésien de la monade de liste, mais le site n'en utilise que deux à la fois, donc c'est plus comme faire liftA2 (,)que le formulaire plus générique traverse.
CA McCann
41

Je pense que c'est plus facile à comprendre en termes de sequenceA, comme traverseon peut le définir comme suit.

traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)
traverse f = sequenceA . fmap f

sequenceA enchaîne les éléments d'une structure de gauche à droite, renvoyant une structure de même forme contenant les résultats.

sequenceA :: (Traversable t, Applicative f) => t (f a) -> f (t a)
sequenceA = traverse id

Vous pouvez également penser sequenceAà inverser l'ordre de deux foncteurs, par exemple passer d'une liste d'actions à une action renvoyant une liste de résultats.

Prend donc une traversecertaine structure et s'applique fà transformer chaque élément de la structure en un applicatif, il séquence ensuite les effets de ces applicatifs de gauche à droite, renvoyant une structure avec la même forme contenant les résultats.

Vous pouvez également le comparer à Foldable, qui définit la fonction associée traverse_.

traverse_ :: (Foldable t, Applicative f) => (a -> f b) -> t a -> f ()

Vous pouvez donc voir que la principale différence entre Foldableet Traversableest que ce dernier vous permet de conserver la forme de la structure, alors que le premier vous oblige à replier le résultat dans une autre valeur.


Un exemple simple de son utilisation consiste à utiliser une liste comme structure traversable et IOcomme applicatif:

λ> import Data.Traversable
λ> let qs = ["name", "quest", "favorite color"]
λ> traverse (\thing -> putStrLn ("What is your " ++ thing ++ "?") *> getLine) qs
What is your name?
Sir Lancelot
What is your quest?
to seek the holy grail
What is your favorite color?
blue
["Sir Lancelot","to seek the holy grail","blue"]

Bien que cet exemple soit plutôt peu intéressant, les choses deviennent plus intéressantes lorsqu'il traverseest utilisé sur d'autres types de conteneurs, ou en utilisant d'autres applicatifs.

Hammar
la source
La traversée est donc simplement une forme plus générale de mapM? En fait, sequenceA . fmappour les listes équivaut à sequence . mapnon?
Raskell
Qu'entendez-vous par «séquençage des effets secondaires»? Quel est «effet secondaire» dans votre réponse - je pensais juste que les effets secondaires ne sont possibles que dans les monades. Cordialement.
Marek
1
@Marek "Je pensais juste que les effets secondaires ne sont possibles que dans les monades" - La connexion est beaucoup plus lâche que cela: (1) Le IO type peut être utilisé pour exprimer des effets secondaires; (2) IOse trouve être une monade, ce qui s'avère très pratique. Les monades ne sont pas essentiellement liées aux effets secondaires. Il convient également de noter qu'il y a une signification de "effet" qui est plus large que "effet secondaire" dans son sens habituel - une signification qui inclut des calculs purs. Sur ce dernier point, voir aussi Que signifie exactement «efficace» .
duplode le
(Au fait, @hammar, j'ai pris la liberté de changer "effet secondaire" en "effet" dans cette réponse pour les raisons exposées dans le commentaire ci-dessus.)
duplode
17

C'est un peu comme fmap, sauf que vous pouvez exécuter des effets dans la fonction mapper, qui modifie également le type de résultat.

Imaginez une liste d'entiers représentant ID utilisateur dans une base de données: [1, 2, 3]. Si vous voulez que fmapces identifiants d'utilisateurs soient des noms d'utilisateur, vous ne pouvez pas utiliser un traditionnel fmap, car à l'intérieur de la fonction, vous devez accéder à la base de données pour lire les noms d'utilisateur (ce qui nécessite un effet - dans ce cas, en utilisant la IOmonade).

La signature de traverseest:

traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)

Avec traverse , vous pouvez faire des effets, par conséquent, votre code pour mapper les ID utilisateur aux noms d'utilisateur ressemble à ceci:

mapUserIDsToUsernames :: (Num -> IO String) -> [Num] -> IO [String]
mapUserIDsToUsernames fn ids = traverse fn ids

Il y a aussi une fonction appelée mapM :

mapM :: (Traversable t, Monad m) => (a -> m b) -> t a -> m (t b)

Toute utilisation de mapMpeut être remplacée partraverse , mais pas l'inverse. mapMne fonctionne que pour les monades, alors que traversec'est plus générique.

Si vous voulez juste pour obtenir un effet et non une valeur utile retour, il y a traverse_et mapM_versions de ces fonctions, toutes deux ne tiennent pas compte de la valeur de retour de la fonction et sont un peu plus rapide.

Kai Sellgren
la source
J'ai peaufiné votre réponse pour les mêmes raisons qui m'ont amené à éditer Hammar's .
duplode le
7

traverse est la boucle. Son implémentation dépend de la structure de données à parcourir. Cela pourrait être une liste, un arbre Maybe,Seq (uence), ou tout ce qui a une façon générique d'être traversée par quelque chose comme une boucle for ou fonction récursive. Un tableau aurait une boucle for, une liste une boucle while, un arbre soit quelque chose de récursif ou la combinaison d'une pile avec une boucle while; mais dans les langages fonctionnels, vous n'avez pas besoin de ces commandes de boucle encombrantes: vous combinez la partie interne de la boucle (sous la forme d'une fonction) avec la structure de données de manière plus directe et moins verbeuse.

Avec la Traversableclasse de types, vous pourriez probablement écrire vos algorithmes de manière plus indépendante et polyvalente. Mais selon mon expérience, cela Traversablen'est généralement utilisé que pour simplement coller des algorithmes aux structures de données existantes. Il est assez agréable de ne pas avoir besoin d'écrire des fonctions similaires pour différents types de données qualifiés.

comonad
la source