需要解释基本的 do 块语法

ICF*_*FSZ 3 monads haskell list-comprehension list do-notation

在ghci中,我写道:

 let x = do
    i <- [1..5]
    j <- [2..4]
    return i 
Run Code Online (Sandbox Code Playgroud)

预期结果:

[1,2,3,4,5]
Run Code Online (Sandbox Code Playgroud)

实际结果:

[1,1,1,2,2,2,3,3,3,4,4,4,5,5,5]
Run Code Online (Sandbox Code Playgroud)

我不明白该输出背后的逻辑。我认为原因可能与 monad 有关,但我对函数式编程很陌生,我希望有人能解释一下。

我也试过 List-comprehension 中的等价形式,结果是一样的,这意味着我在这里误解了一些基本的东西。

jpm*_*ier 6

这是因为 do 机制并不关心(幸运的是)最里面的代码是否真的引用了(一些)循环变量。

看你总是得到 3*5=15 个值,不管最里面的代码:

 ?> 
 ?> 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
 ?> 

Run Code Online (Sandbox Code Playgroud)

据我所知,这是完全标准的行为,Haskell 与 C、C++、Fortran、Python 共享......

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;
}

Run Code Online (Sandbox Code Playgroud)

C++ 输出:

$ ./a.out
1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 5, 5, 
$ 

Run Code Online (Sandbox Code Playgroud)


lef*_*out 5

我也试过 List-comprehension 中的等效形式,结果是一样的

好主意。碰巧的是,对于列表,do符号与列表推导式完全相同。(事实上​​,有一个语法扩展允许你对任何 monad 使用列表理解符号,就像你可以do对任何 monad使用符号一样。)

所以,你问为什么[a | a<-[0,1], b<-[2,3]]给出[0,0,1,1]而不是[0,1]. 这看起来令人惊讶的方式是,如果您将列表推导视为集合推导,就像您在数学中找到的那样。但是列表不是集合,尽管 Haskeller 确实经常使用列表作为集合的临时替代品。如果列表推导式充当集合推导式,那么

  [x | x <- [0,1,0]]
Run Code Online (Sandbox Code Playgroud)

也应该只[0,1]作为它的结果产生(或者至少,它应该产生相同的结果[x|x<-[0,1]])。

通常,这种清除重复项需要相等性检查,如果您想使其高效,还可以使用排序或散列方法。列表不会做任何这样的事情,所以如果你想要类似集合的行为,你应该使用一个集合实现的数据结构。Set并且HashSet是最常见的。