web-dev-qa-db-fra.com

Quelqu'un peut-il expliquer la fonction de cheminement dans Haskell?

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.

90
Konan

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.

109
Sjoerd Visscher

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 Applicatives 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]]
53
Landei

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.

41
hammar

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.

16
Kai Sellgren

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

7
comonad