【问题标题】:In Haskell what are the inner workings of list comprehension?在 Haskell 中,列表理解的内部工作原理是什么?
【发布时间】:2015-05-13 01:09:14
【问题描述】:

我是 Haskell 的新手,我在这里查看了这篇文章:Cartesian product of 2 lists in Haskell

在答案中有这个sn-p的代码:

cartProd xs ys = [(x,y) | x <- xs, y <- ys]

这两个列表中的哪一个:

xs = [1,2,3]
ys = [4,5,6]

会产生

[(1,4),(1,5),(1,6),(2,4),(2,5),(2,6),(3,4),(3,5),(3,6)]

如果我没有看到这个结果,我会认为它会返回

[(1,4),(2,5),(3,6)]

因为它会同时遍历两个列表。

但现在它 - 对于我更了解的编程语言 - 看起来像是用于遍历矩阵的双 for 循环:

for (int x = 1; x < 4; x++)
    for(int y = 4; y < 7; y++)
        //make tuple (x,y)

是什么导致列表推导以这种方式运行?

【问题讨论】:

  • 首先,列表不是主动构造的,而是使用惰性求值...
  • 您能详细说明一下吗?我在非常基本的层面上了解惰性评估,但还不知道它的功能。
  • 嗯,列表不是“完全”生成的(实际上根本没有生成)。它只是存储“定义”,当你对第一个元素感兴趣时,它会计算第一个元素,并为剩余部分构造一个新的表达式,等等。因此你甚至可以定义无限长的列表。
  • 好吧,我想我明白了,但这对为什么 x 保持不变而 y 增加有任何影响吗?
  • @AR7 惰性求值永远不会影响产生哪个值;所有终止的评估订单将产生相同的值。惰性求值的神奇之处在于,如果任何求值顺序终止,惰性求值也会终止。简而言之:不,惰性求值对为什么 x 保持不变而 y 增加没有影响。

标签: list haskell list-comprehension


【解决方案1】:

这个introduction 解释了列表理解的语法。基本上可以说每个x &lt;- list 意味着一个额外的嵌套“for”循环来生成元组,并且每个谓词都经过简单的测试。因此表达式:

[(x,y) | x <- xs, even x, y <- ys, even 3*y-div x 2]

将被翻译成命令式语言:

for (var x : xs) {
    if(even(x)) {
    for(var y : ys) {
        if(even(3*y-x/2)) {
            yield (x,y)
        }
    }
}

yield 是有时与协程一起使用的关键字。此外,对于yield,评估是懒惰的。例如,这可以生成 all 偶数整数:

[x|x <-[2..],even x]

列出单子

为了从根本上理解列表理解,需要知道Monads 是什么。每个列表推导式都可以翻译成 list monad。例如你的例子被翻译成:

do x <- xs
   (do y <- ys
       return (x,y))

这又是语法糖:

xs >>= (\x -> (ys >>= \y -> return (x,y)))

monad 是函数式编程中的一个重要概念(可能更好地阅读维基百科页面),因为它有点难以掌握。有时说一个 monads 就像墨西哥卷饼,....

一旦你或多或少地理解了一个 monad:一个 monad 是一个类型类,带有一个 return 语句和一个 &gt;&gt;= 通道语句。现在内部的return 语句很简单:

return x = [x]

这意味着每次设置xy 时,您将创建一个元组(x,y) 并将其作为单例列表返回:因此[(x,y)]。现在“绑定”运算符&gt;&gt;= 需要将ys\y -&gt; return (x,y)“粘合”在一起。这是通过将其实现为:

(>>=) xs f = concat $ map f xs

换句话说,您进行映射并连接映射的结果。

现在如果考虑不加糖的表达式的第二部分:

ys >>= \y -> return (x,y)

这意味着对于给定的x(我们现在抽象掉),我们会将ys 中的每个元素映射到一个元组(x,y) 并返回它。因此,我们将生成一个列表列表,每个列表都是包含一个元组的单例。类似的东西(如果ys=[1,2]):

[[(x,1)],[(x,2)]]

现在&gt;&gt;= 将进一步concat 变成:

\x -> [(x,1),(x,2)]

到目前为止,我们已经将x 抽象出来(假设它是一个)。但现在我们可以取该表达式的第一部分:

xs >>= \x -> [(x,1),(x,2)]

如果xs=[3,5],则表示我们将重新创建列表:

[[(3,1),(3,2)],[(5,1),(5,2)]]

在 concat 之后:

[(3,1),(3,2),(5,1),(5,2)]

这是我们的期望:

[(x,y)|x<-[3,5],y<-[1,2]]

【讨论】:

  • 感谢您的解释。我一直在阅读“Learn You A Haskell”,但我还没有进入 Monads,这可能是我感到困惑的原因。
  • 嗯,monad 部分有点难,但它对于深入理解一些函数式编程概念很重要。阅读 monads 后,你开始明白 Haskell 的大部分内容是 monadic 概念的语法糖。
  • @AR7 没有必要全面理解 monad 来理解列表推导。另一个答案给出了 Haskell 报告对列表理解的实际脱糖翻译(这比这个答案中建议的更完整),并且提到 monads 恰好零次。
  • 可以这样翻译,但事实并非如此。事实上,在最新的 GHC 版本中,它最终走向了完全相反的方向!列表推导实际上以两种不同的方式之一去糖,其中一种充满了对 foldrbuild 的调用,另一种充满了递归函数。
  • 另一方面,Haskell 报告用concatMap 解释列表推导。
【解决方案2】:

引用 Haskell 报告,列表推导的评估如下:

[ e | True ]   = [e]
[ e | q ]      = [ e | q, True ]
[ e | b,  Q  ] = if b then [ e | Q ] else []
[ e | p <- l,  Q ] = let ok p = [ e | Q ]
                         ok _ = []
                     in concatMap ok  l
[ e | let decls,  Q ] = let decls in [ e | Q ]

在您的情况下,相关部分是,因为模式 p 只是一个变量 x

[ e | x <- l, Q ] = concatMap (\x -> [ e | Q ]) l

更具体地说,理解 [(x,y) | x &lt;- xs, y &lt;- ys] 被翻译成

concatMap (\x -> [(x,y) | y <- ys]) xs

根据concatMap的定义,它是

concat (map (\x -> [(x,y) | y <- ys]) xs)

让我们用具体值替换xs,ys

concat (map (\x -> [(x,y) | y <- [4,5,6]]) [1,2,3])

申请map:

concat [ [(1,y) | y <- [4,5,6]] 
       , [(2,y) | y <- [4,5,6]] 
       , [(3,y) | y <- [4,5,6]] ]

评估内部列表推导:(这些可以使用上面的定律再次翻译,但我会简短)

concat [ [(1,4),(1,5),(1,6)]
       , [(2,4),(2,5),(2,6)]
       , [(3,4),(3,5),(3,6)] ]

通过连接上面的列表,我们得到了结果,

       [  (1,4),(1,5),(1,6) 
       ,  (2,4),(2,5),(2,6) 
       ,  (3,4),(3,5),(3,6)  ]

请注意,GHC 还作为 Haskell 扩展实现了所谓的并行列表推导,它确实按您的预期运行:

> :set -XParallelListComp
> [(x,y)| x<-[1,2,3] | y <-[4,5,6]]
[(1,4),(2,5),(3,6)]

在内部,他们使用zip(或者更确切地说,zipWith)函数。

【讨论】:

    【解决方案3】:

    列表解析语法背后的思想来自数学中的set-builder notation

    在数学中,可以这样写:

    { (x, y) | x ∈ xs, y ∈ ys }
    

    表示“所有元素的集合,形式为 (x, y),其中 x 是集合 xs 的一个元素,y 是集合 ys 的一个元素”。

    现在我们想把这个集合构建器的想法变成编程语言的“列表构建器”语法。所以我们想要这个:

    [ (x, y) | x <- xs, y <- ys ]
    

    表示“包含 (x, y) 形式的所有元素的列表,其中 x 是从 xs 获得的元素,y 是从 ys 获得的元素”。

    很明显,如果这个列表表示法产生了列表[(1,4),(2,5),(3,6)](当xs = [1, 2, 3]ys = [4, 5, 6] 时)它根本不会是所需形式的所有对:(1, 6) 是一个可以从@ 获得的元素987654328@ 与可以从 ys 获得的元素配对,但它不在您的列表中。

    所以对于“是什么导致列表理解以这种方式运行?”的真正陈词滥调的答案是什么?是不是它被编程以这种方式运行,因为这就是想出它的人希望它的行为方式。

    您可以通过多种不同的方式对这种行为进行编程。您可以(正如 OP 注意到的那样)使用嵌套循环,或者您可以使用 mapconcat 之类的函数,或者您可以将 Monad 实例用于列表等。因此,您可以将列表理解语法转换为任何这些是为了证明列表推导是“真正的单子”或“真正的嵌套循环”或其他。

    但从根本上说,列表推导式是组合式的,而不是压缩式的,因为语言设计者特意选择了表达该含义的语法,以便它有点像数学中的集合构建符号1。 “x 保持不变,而 y 递增”不仅仅是实现的一个有趣的怪癖,该实现是选择专门为此而设计的。如果有一些替代宇宙,其中列表单子(或嵌套循环等)对此目的没有用,列表推导式不会在该宇宙中产生不同的结果,它们将通过其他方式实现以产生相同的结果。


    1 列表解析语法和集合生成器表示法之间存在显着差异,尽管它们看起来很相似。一个根本的区别来自使用列表而不是集合。一个元素要么在集合中,要么不在集合中,但列表包含特定索引处的元素(包括在多个索引处包含给定元素的可能性)。

    所以列表推导式语法必须定义 order 它将产生其元素(以及它将产生多少元素),这使得列表推导式从根本上与 枚举 ,而 set-builder 表示法基本上是关于 membership。 set-builder 符号{ (x, y) | x ∈ xs, y ∈ ys } 真正的意思是“你可以通过检查 x ∈ xs 和 y ∈ ys 是否在我们正在构建的集合中来回答给定的 (x, y) ”,而列表理解 [ (x, y) | x &lt;- xs, y &lt;- ys ]是说“您可以通过枚举xs 来枚举我们正在构建的列表的元素,然后对每个元素也枚举ys

    【讨论】:

    • 我们可以更抽象地看到 LC 符号,作为“袋子”符号:列表,允许重复,但没有与元素关联的位置。毕竟,如果没有明确使用zip,LC 表示法中的任何内容都无法为我们提供位置。该表示法适用于任何数据类型,事实上它可以使用MonadComprehensions。作为一个例子,一种语言可以有一个“非确定性集合”(内置的、不透明的)类型,它可以并行执行所有操作。地图[f x | x &lt;- xs] 仍然可以很好地定义。实际上在 Scheme 中,map 的 eval 顺序是未知/未指定的。
    • @WillNess 这一切都是正确的,但元素的顺序显然是 Haskell 列表接口的一部分,因此任何生成列表(不是不纯的)的表达式都需要产生一个很好的结果- 确定的顺序。 MonadComprehensions 将语法概括为不同类型的不同含义,但对于列表,它仍然意味着组合枚举(如果您将列表视为集合,则为元素,或者如果您有“列表单子模型非确定性”帽子,则为可能性)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-05-15
    • 2013-05-01
    • 1970-01-01
    • 2010-11-30
    • 1970-01-01
    • 2013-04-04
    • 1970-01-01
    相关资源
    最近更新 更多