优化咖喱需要点自由风格

时间:2017-01-21 08:16:34

标签: haskell ghc currying pointfree partial-application

假设我们有一个(人为的)功能:

connections

我们将其部分应用于其他地方,例如:

import Data.List (sort)

contrived :: Ord a => [a] -> [a] -> [a]
contrived a b = (sort a) ++ b

从表面上看,这可以达到预期效果:

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

但是,如果我们将[[1,2,3,4],[1,2,3,5],[1,2,3,6]] 放入:{/ p>

trace

我们看到传入import Debug.Trace (trace) contrived :: Ord a => [a] -> [a] -> [a] contrived a b = (trace "sorted" $ sort a) ++ b map (contrived $ trace "a value" [3,2,1]) [[4],[5],[6]] 的第一个列表只评估了一次,但是对contrived中的每个项目进行了排序:

[4,5,6]

现在,[sorted a value [1,2,3,4],sorted [1,2,3,5],sorted [1,2,3,6]] 可以简单地转换为无点样式:

contrived

部分应用时:

contrived :: Ord a => [a] -> [a] -> [a]
contrived a = (++) (sort a)

仍然按照我们的预期运作:

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

但如果我们再次添加[[1,2,3,4],[1,2,3,5],[1,2,3,6]] s:

trace

我们看到现在传入contrived :: Ord a => [a] -> [a] -> [a] contrived a = (++) (trace "sorted" $ sort a) map (contrived $ trace "a value" [3,2,1]) [[4],[5],[6]] 的第一个列表仅被评估并排序一次:

contrived

为什么会这样?由于转换为无点样式是如此微不足道,为什么GHC不能推断它只需要在[sorted a value [1,2,3,4],[1,2,3,5],[1,2,3,6]] 的第一个版本中对a进行一次排序?

注意:我知道,对于这个相当简单的例子,它可能更适合使用无点样式。这是一个人为的例子,我已经简化了很多。当以无点样式表达时,我遇到问题的真正功能不太明确(在我看来):

contrived

在无点样式中,这需要在realFunction a b = conditionOne && conditionTwo where conditionOne = map (something a) b conditionTwo = somethingElse a b 周围编写一个丑陋的包装器(both):

(&&)

顺便说一句,我也不确定为什么realFunction a = both conditionOne conditionTwo where conditionOne = map (something a) conditionTwo = somethingElse a both f g x = (f x) && (g x) 包装器有效; both的无点样式的行为类似于realFunction的无点样式版本,因为部分应用程序仅被评估一次(即如果contrived排序something它只会这样做一旦)。看来,由于a不是无点免费的,因此Haskell应该与非点免费both具有相同的问题。

1 个答案:

答案 0 :(得分:2)

如果我理解正确,你正在寻找:

contrived :: Ord a => [a] -> [a] -> [a]
contrived a = let a' = sort a in \b -> a' ++ b
                    -- or ... in (a' ++)

如果您希望仅对计算排序一次,则必须在\b之前完成。

你是正确的,编译器可以优化它。这被称为“完全懒惰”优化。

如果我没记错的话,GHC并不总是这样做,因为在一般情况下,它并不总是真正的优化。考虑人为的例子

foo :: Int -> Int -> Int
foo x y = let a = [1..x] in length a + y

当传递两个参数时,上面的代码在常量空间中工作:列表元素在生成时立即被垃圾收集。

当部分应用x时,foo x的闭包仅需要O(1)内存,因为尚未生成列表。代码如

let f = foo 1000 in f 10 + f 20  -- (*)

仍在恒定的空间中运行。

相反,如果我们写了

foo :: Int -> Int -> Int
foo x = let a = [1..x] in (length a +)

然后(*)将不再在恒定空间中运行。第一个呼叫f 10将分配一个1000长的列表,并将其保留在内存中以进行第二次呼叫f 20

请注意您的部分申请

... = (++) (sort a)

基本上意味着

... = let a' = sort a in \b -> a' ++ b

因为参数传递涉及绑定,如let中所示。因此,sort a的结果将保留所有未来的电话。