Besoin d'explications pour la syntaxe de base des blocs do
Dans ghci, j'ai écrit:
let x = do
i <- [1..5]
j <- [2..4]
return i
Résultat attendu:
[1,2,3,4,5]
Résultat actuel:
[1,1,1,2,2,2,3,3,3,4,4,4,5,5,5]
Je ne comprends pas la logique derrière cette sortie. Je pense que la raison pourrait être quelque chose à propos de la monade, mais je suis très nouveau dans la programmation fonctionnelle, j'aimerais que quelqu'un puisse l'expliquer un peu.
J'ai également essayé la forme équavalente dans la compréhension de liste et le résultat est le même, ce qui signifie qu'il y a quelque chose de fondamental que j'ai mal compris ici.
Réponses
J'ai aussi essayé la forme équavalente en List-compréhension et le résultat est le même
Bonne idée. Il se trouve que pour les listes, la do
notation fait exactement la même chose que les compréhensions de listes. (En fait, il existe une extension syntaxique qui vous permet d'utiliser la notation de compréhension de liste pour n'importe quelle monade, comme vous pouvez utiliser la do
notation pour n'importe quelle monade.)
Donc, vous demandez pourquoi [a | a<-[0,1], b<-[2,3]]
donne [0,0,1,1]
au lieu de [0,1]
. La façon dont cela semble surprenant, c'est si vous pensez aux compréhensions de listes comme des compréhensions d' ensemble comme vous en trouverez en mathématiques. Mais les listes ne sont pas des ensembles, bien que les Haskellers utilisent souvent des listes comme substitut improvisé pour les ensembles. Si les listes de compréhension agissaient comme une compréhension d'ensemble, alors
[x | x <- [0,1,0]]
devrait également donner seulement [0,1]
comme résultat (ou du moins, il devrait donner le même résultat que [x|x<-[0,1]]
).
En général, ce type d'élimination des doublons nécessite des vérifications d'égalité, et si vous voulez le rendre efficace, vous pouvez également utiliser une méthode de tri ou de hachage. Les listes ne font rien de tel, donc si vous voulez un comportement semblable à un ensemble, vous devez utiliser une structure de données implémentant un ensemble. Setet HashSetsont les plus courants.
Ceci est dû au fait que le mécanisme do ne se soucie pas (heureusement) de savoir si le code le plus profond fait réellement référence à (certaines) des variables de boucle.
Voyez-vous toujours obtenir 3 * 5 = 15 valeurs quel que soit le code le plus profond:
λ>
λ> xs1 = do { i <- [1..5] ; j <- [2..4] ; return i }
λ> xs1
[1,1,1,2,2,2,3,3,3,4,4,4,5,5,5]
λ>
λ> xs2 = do { i <- [1..5] ; j <- [2..4] ; return 9 }
λ> xs2
[9,9,9,9,9,9,9,9,9,9,9,9,9,9,9]
λ>
λ> xs3 = do { i <- [1..5] ; j <- [2..4] ; return (i,j) }
λ> xs3
[(1,2),(1,3),(1,4),(2,2),(2,3),(2,4),(3,2),(3,3),(3,4),(4,2),(4,3),(4,4),(5,2),(5,3),(5,4)]
λ>
λ> length xs1
15
λ> length xs2
15
λ> length xs3
15
λ>
Autant que je sache, c'est un comportement parfaitement standard, que Haskell partage avec C, C ++, Fortran, Python ...
Un exemple équivalent C ++:
#include <vector>
#include <iostream>
int main()
{
std::vector<int> vi{1,2,3,4,5};
std::vector<int> vj{2,3,4};
for (int i: vi)
for (int j: vj)
std::cout << i << ", ";
std::cout << std::endl;
return EXIT_SUCCESS;
}
Sortie C ++:
$ ./a.out 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, $