Haskell ghci使用什么优化技术来加速递归映射?

Haskell ghci使用什么优化技术来加速递归映射?,haskell,optimization,expression,Haskell,Optimization,Expression,假设我有以下功能: minc = map (+1) natural = 1:minc natural 看起来是这样展开的: 1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc... 1:2:minc(minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc... 1:2:minc(2:minc(2:minc(2:minc(2:minc(2:mi

假设我有以下功能:

minc = map (+1)
natural = 1:minc natural
看起来是这样展开的:

1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc...
1:2:minc(minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc...
1:2:minc(2:minc(2:minc(2:minc(2:minc(2:minc(2:minc(2:minc(2:minc(minc...
1:2:3:minc(3:minc(3:minc(3:minc(3:minc(3:minc(3:minc(3:minc(minc(minc...
...                                                                
(!!n) $ 1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc...
(!!n-1) $ minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc...
(!!n-1) $ (1+1):minc(minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc...
            -- note that `(1+1)` isn't actually calculated!
(!!n-2) $ minc(minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc...
(!!n-2) $ ((1+1)+1):minc(minc(minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc...
            -- again, neither of the additions is actually calculated.
(!!n-3) $ minc(minc(minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc...
(!!n-3) $ ((...)+1):minc(minc(minc(minc(1:minc(1:minc(1:minc(1:minc(1:minc...
...
(!!n-n) $ ((...+1)+1) : minc(minc(...minc(minc(1:minc(...
           ╰─ n ─╯
(!!0) $ (n+1) : _
n+1
虽然它是惰性地评估的,但要构建列表中的每个新数字,必须展开一个表达式
n
次,这给了我们
O(n^2)
复杂性。但从执行时间来看,我可以看出真正的复杂性仍然是线性的

Haskell在本例中使用了哪种优化,以及它如何展开此表达式

要构建列表中的每个新数字,必须展开一个表达式
n
次,这给了我们O(N2)的复杂性

不完全是。以这种方式展开前N个数的复杂性实际上是O(N2)显然我错了。但是如果您只请求第N个数字,那么它的实际计算结果如下:

1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc...
1:2:minc(minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc...
1:2:minc(2:minc(2:minc(2:minc(2:minc(2:minc(2:minc(2:minc(2:minc(minc...
1:2:3:minc(3:minc(3:minc(3:minc(3:minc(3:minc(3:minc(3:minc(minc(minc...
...                                                                
(!!n) $ 1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc...
(!!n-1) $ minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc...
(!!n-1) $ (1+1):minc(minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc...
            -- note that `(1+1)` isn't actually calculated!
(!!n-2) $ minc(minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc...
(!!n-2) $ ((1+1)+1):minc(minc(minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc...
            -- again, neither of the additions is actually calculated.
(!!n-3) $ minc(minc(minc(1:minc(1:minc(1:minc(1:minc(1:minc(1:minc...
(!!n-3) $ ((...)+1):minc(minc(minc(minc(1:minc(1:minc(1:minc(1:minc(1:minc...
...
(!!n-n) $ ((...+1)+1) : minc(minc(...minc(minc(1:minc(...
           ╰─ n ─╯
(!!0) $ (n+1) : _
n+1
每增加N只需要固定数量的两个步骤,加上达到指数后的N个加法,这仍然是O(N)

这里的关键是,
map
基本上只对整个列表应用一次。它是完全懒惰的,即,要产生
\u:\ u
thunk,它只需要知道列表的长度至少为1,但实际的元素根本不重要

这样,我们编写的
minc(minc(…)(minc(1:…
)在一个步骤中就被
(…+1):minc(…
替换了



[1] 结果表明,即使我们对前N个数字求和,也是在O(N)中完成的。我不知道怎么做。

自然列表在每个递归步骤之间共享。图形的计算如下所示

1:map (+1) _
 ^         |
 `---------'

1: (2 : map (+1) _)
      ^          |
      `----------'

1: (2 : (3 : map (+1) _)
           ^          |
           `----------'

这种共享意味着代码使用O(n)时间,而不是预期的O(n^2).

我编译并计时:10000000得到1,10000000得到12.94s。看起来很线性。你是如何得到结果的?没关系,我说的完全是假的。嗯,我知道它如何通过N步索引计算列表中的一个元素,但调用
take
时它如何计算所有元素?似乎在thunks expr中Session为
N+1
计算的部分与为
N
计算的部分不共享。当取第一个N数时,测试它是否真的是O(N^2)的正确方法是什么?我在做
(sum.take 100000000)natural
。嗯,看起来确实可以在O(N)中对前N个元素求和。这太了不起了!现在还不能说它是如何工作的……所有这些计算都是共享的,只有一个
minc
列表,并且不需要在几个点上重新计算它的内容。事实上,在我的示例中,具有线性复杂性意味着存在某种列表共享。但是GHC如何具体知道何时以及如何操作timize?它专门寻找这样的情况吗?GHC甚至通常不会消除常见的子表达式,所以我很惊讶这里会有某种形式的记忆。@egdmitry这不是真正的优化,只是懒惰(包括不多次计算表达式)thunks循环地引用他们自己,因为他们是这样定义的。基本上,这是每个Haskell编译器都应该做的,即使它不是特别聪明。没有“优化”在这里,共享评估的唯一原因是因为只有一个
natural
,内存中只有一个位置存在此列表。起初它只是一个thunk
1:minc natural
,但当您评估第二个元素时,它会变成一个包含1个cons
map(+1)(1:ref到natural的尾部)
1 cons 2 cons
map(+1)(2:ref to tail of tail of natural)
,如您所见,下一步将是O(1),依此类推。有趣的是,
test x=x:map(+1)(test x)
是O(N^2)。因此,一旦有一个参数,GHC就看不到这是同一个thunk?这是正确的。GHC通常只会在thunk直接引用同一事物时共享thunk。如果我们编写
test x=let test'=x:map(+1),您的测试将获得共享测试“在x
中。在本例中,我们可以看到测试”可以共享。此转换称为静态参数转换,并且已经讨论过在GHC中启用它