点无代码更高效还是只是更简洁?

25

我编写了以下代码,它接收一堆点并使用gloss库将它们绘制在屏幕上。

let s = blocks pes
    pts = map (map mkPt) s  {- stitches to points-}
    lines = map Line pts    {-points to lines -}
    pict = Pictures lines   {- lines to a picture -}
  in do  displayInWindow "My Window" (200, 200) (10, 10) white pict

它运行正常,但我意识到有一个重复的模式:一系列的函数调用,每个函数调用的结果馈入下一个函数调用的最后一个参数。 因此,我进行了重构,通过删除中间变量,反转顺序并使用函数组合(“.”)链接这些函数,如下所示:

let pict = Pictures . (map Line) . (map $ map $ mkPt) . blocks $ pes
                in do  displayInWindow "My Window" (200, 200) (10, 10) white pict

很高兴,这也完全可行。但是我想知道我是否在牵强阅读能力,或者我只是不习惯阅读和编写点式风格的代码。此外,我如何思考这段代码?第二个版本更有效还是更简洁?有什么样式上的事情可以使它更清晰吗?

2个回答

31

以下是几个快速建议:

let pict = Pictures . (map Line) . (map $ map $ mkPt) . blocks $ pes
in do displayInWindow "My Window" (200, 200) (10, 10) white pict

你有一些可以直接删除的多余内容:

let pict = Pictures . map Line . (map $ map mkPt) . blocks $ pes
in do displayInWindow "My Window" (200, 200) (10, 10) white pict

你用map (map mkPt)这个表达式时本来就没有避免使用括号的必要,所以去掉$符号:

let pict = Pictures . map Line . map (map mkPt) . blocks $ pes
in do displayInWindow "My Window" (200, 200) (10, 10) white pict

为了清晰明了,您可以将组合链写在多行上:

let pict = Pictures 
         . map Line 
         . map (map mkPt) 
         . blocks $ pes
in do displayInWindow "My Window" (200, 200) (10, 10) white pict

这个do块是多余的,因为它只有一条语句,你可以将最终的应用程序移动到定义之外:

let displayPict = displayInWindow "My Window" (200, 200) (10, 10) white
                . Pictures 
                . map Line 
                . map (map mkPt) 
                . blocks
in displayPict pes
您可以合并这两个map:
let displayPict = displayInWindow "My Window" (200, 200) (10, 10) white
                . Pictures 
                . map (Line . map mkPt) 
                . blocks
in displayPict pes

有时候对于较长的链式操作,使用来自Control.Arrow 的反向组合运算符会更加可读:

let displayPict = blocks
                  >>> map (Line . map mkPt) 
                  >>> Pictures
                  >>> displayInWindow "My Window" (200, 200) (10, 10) white
in displayPict pes

但这一切都是可选的;根据自己的口味来调整代码。

关于效率,我认为在 GHC 优化器处理完代码后,两者不会有任何区别。


4
实际上,点释放版本的生成代码应该是相同的。没有任何优化的情况下,点释放代码的效率会稍微低一些,因为它有更多的函数调用(组合)。 - augustss
1
@augustss:感谢确认 - 我不确定,但重新排列都相当琐碎,似乎没有任何差异,就可能的子表达式重新计算而言。除非差别很大,例如依赖于严格性分析器将foldl转换为foldl',否则我不会担心“无优化”情况。 - C. A. McCann
2
@nont:没有proc符号的Arrow通常需要大量使用pointfree风格,因此其中有几个组合子是有用的--只需将类型签名中的Arrow实例视为(->),例如(Arrow a) => a b c -> a b d -> a b (c, d)变成了(b -> c) -> (b -> d) -> b -> (c, d) - C. A. McCann
2
值得一提的是,let表达式没有任何实际作用,只是为了使其无需指定参数。整个代码可以写成displayInWindow "My Window" (200, 200) (10, 10) white . Pictures . map (Line . map mkPt) . blocks $ pes - HaskellElephant
2
此外,(>>>) 可以由 Control.Arrow 导出,但它是在 Control.Category 中定义的。它也被定义在 Category 类型类中,Arrow 是它的一个特化实例。所有的 Arrows 实例都是 Category,但反之不成立——比如 Lens。总之,如果你不打算使用与 Arrows 特定相关的内容,我建议直接使用 Control.Category。 - Tony Morris
显示剩余2条评论

4

无参式风格可以很清楚地展示一个函数只是对输入进行一系列转换。例如,阅读以下内容非常容易:

foo = map show . doThis . doThat . etc

因为它看起来像典型的无参函数式编程代码,熟悉这种编程方式的人可以清楚地看到重要的部分,没有杂音。与此相比:

foo x = d
    where
        a = etc x
        c = doThis b
        b = doThat a
        d = map show c

显然,这有点人为,但是思路是需要仔细阅读和关注额外的定义,以便理解foo的真正作用。 a是否被用作多个辅助函数的参数?b呢?数据是否按照预期的方式通过这些辅助函数流动?在这种情况下,点自由风格是说“事物只是在管道中转换,你不需要考虑任何奇怪的事情”。
在其他情况下,点自由风格可能会使事情变得更加晦涩。通常情况下,您需要使用let和where。因此,简洁并不是唯一的目标,减少心理负担也很重要。(正如其他人评论的那样,点自由风格在优化代码中不应该有性能差异)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接