J'essaie et je n'arrive pas à bloquer la fonction traverse
de 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.
traverse
est 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 du Data.Traversable
Documentation.
data Tree a = Empty | Leaf a | Node (Tree a) a (Tree a)
L'instance Functor
de Tree
serait:
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'instance Traversable
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'application est presque la 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 de 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 classe Traversable
contient également une version monadique de traverse
appelée mapM
. À toutes fins utiles, mapM
est identique à traverse
- il existe en tant que méthode distincte car Applicative
n'est devenu plus tard qu'une superclasse de Monad
.
Si vous implémentez ceci dans un langage impur, fmap
serait le même que traverse
, car il n'y a aucun moyen d'empêcher 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 de la façon dont 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 cela vous limite cependant aux effets que le langage permet. Si vous f.e. voulez le non-déterminisme (dont l'instance de liste des modèles applicatifs) et votre langue ne l'a pas intégré, vous n'avez pas de chance.
traverse
transforme les choses à l'intérieur d'un Traversable
en Traversable
de choses "à l'intérieur" d'un Applicative
, étant donné une fonction qui rend Applicative
s hors de des choses.
Utilisons Maybe
comme Applicative
et 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 d'un 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 en est que la fonction <*>
Est utilisée pour générer le résultat, et lorsque l'un des arguments est Nothing
, nous récupérons Nothing
.
Un autre exemple:
rep x = replicate x x
Cette fonction génère une liste de longueur x
avec le contenu x
, par ex. rep 3
= [3,3,3]
. Quel est le résultat de traverse rep [1..3]
?
Nous obtenons les résultats partiels de [1]
, [2,2]
Et [3,3,3]
En utilisant rep
. Maintenant, la sémantique des listes comme Applicatives
est "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]
Sont six fois [1,2,3]
. Nous avons donc:
traverse rep [1..3]
--[[1,2,3],[1,2,3],[1,2,3],[1,2,3],[1,2,3],[1,2,3]]
Je pense qu'il est plus facile à comprendre en termes de sequenceA
, car traverse
peut être défini 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
comme inversant l'ordre de deux foncteurs, par ex. passer d'une liste d'actions à une action renvoyant une liste de résultats.
Donc traverse
prend une certaine structure, et applique f
pour transformer chaque élément de la structure en un applicatif, puis séquence 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 Foldable
et Traversable
est que cette dernière vous permet de conserver la forme de la structure, tandis que la première vous oblige à replier le résultat dans une autre valeur .
Un exemple simple de son utilisation est d'utiliser une liste comme structure traversable et IO
comme application:
λ> 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 passionnant, les choses deviennent plus intéressantes lorsque traverse
est utilisé sur d'autres types de conteneurs, ou en utilisant d'autres applications.
C'est un peu comme fmap
, sauf que vous pouvez exécuter des effets à l'intérieur de la fonction mapper, qui modifie également le type de résultat.
Imaginez une liste d'entiers représentant les ID utilisateur dans une base de données: [1, 2, 3]
. Si vous voulez fmap
ces ID utilisateur pour les noms d'utilisateur, vous ne pouvez pas utiliser un fmap
traditionnel, 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 monade IO
).
La signature de traverse
est:
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 à:
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 mapM
peut être remplacée par traverse
, mais pas l'inverse. mapM
ne fonctionne que pour les monades, tandis que traverse
est plus générique.
Si vous voulez juste obtenir un effet et ne renvoyer aucune valeur utile, il y a traverse_
et mapM_
versions de ces fonctions, qui ignorent toutes les deux la valeur de retour de la fonction et sont légèrement plus rapides.
traverse
is la boucle. Son implémentation dépend de la structure de données à parcourir. Cela peut être une liste, un arbre, Maybe
, Seq
(uence), ou tout ce qui a une manière générique d'être traversé via quelque chose comme une boucle for ou une 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 intérieure 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 classe de types Traversable
, vous pourriez probablement écrire vos algorithmes plus indépendants et polyvalents. Mais mon expérience montre que Traversable
n'est généralement utilisé que pour coller simplement 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.