【问题标题】:Point Free Style Required for Optimized Curry优化咖喱所需的无点风格
【发布时间】:2017-01-21 08:16:33
【问题描述】:

假设我们有一个像这样的(人为的)函数:

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

但是,如果我们将一些 traces 放入:

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

但是如果我们再次添加traces:

contrived :: Ord a => [a] -> [a] -> [a]
contrived a = (++) (trace "sorted" $ sort a)
map (contrived $ trace "a value" [3,2,1]) [[4],[5],[6]]

我们看到现在传入contrived 的第一个列表只被评估和排序一次:

[sorted
a value
[1,2,3,4],[1,2,3,5],[1,2,3,6]]

为什么会这样?既然翻译成pointfree风格那么琐碎,为什么GHC不能推断出在contrived的第一版中它只需要对a进行一次排序呢?


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

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 的 pointfree 样式的行为类似于 contrived 的 pointfree 样式版本,因为部分应用程序只评估一次(即,如果 something 排序 a 它只会这样做一次)。看来,由于both 不是无点的,Haskell 应该有与非无点的contrived 相同的问题。

【问题讨论】:

    标签: haskell ghc currying pointfree partial-application


    【解决方案1】:

    如果我理解正确,您正在寻找这个:

    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 的结果将保留以供将来所有调用使用。

    【讨论】:

    • +1(如果可以的话+更多!)感谢您提供非常清晰的解释。我明白为什么 GHC 在一般情况下默认不进行这种优化。我忘了提到我尝试使用($!) 来强制对排序进行严格评估。我一定是做错了,因为你瞧,($!) 只是你建议的 let 构造的另一种拼写!
    • @BaileyParker 实际上,let 不会立即强制求值,而 $! 会强制争论。 let x = e in f x + g x 将仅在 f,g 需要时评估 e,但会将其值保存在内存中以供第二次调用。
    猜你喜欢
    • 2020-07-22
    • 1970-01-01
    • 2018-02-02
    • 1970-01-01
    • 2018-11-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多