在 point-free 风格中编写简单的 Haskell 函数

13

我正在尝试理解如何将Haskell中的函数转换为点无关表示法。我看到了这个例子,但它比我想要的更加复杂。我感觉我理解了它背后的逻辑,但当我尝试在代码中执行一些简单的例子时,我遇到了编译错误。我想尝试用点无关风格来编写这个函数:

f x = 5 + 8/x,我重新排列为f x = (+) 5 $ (/) 8 x

所以,我认为它可能是这样的:

f = (+) 5 $ (/) 8

但是当我在ghci中运行时,会收到以下消息:

No instance for (Num (a0 -> a0))
  arising from the literal `5' at Test.hs:3:9
Possible fix: add an instance declaration for (Num (a0 -> a0))
In the first argument of `(+)', namely `5'
In the first argument of `($)', namely `(+) 5'
In the expression: (+) 5 $ (/) 8
Failed, modules loaded: none.

我不理解"No instance for..."的信息。我需要怎么做才能以点无形式编写此函数?


4
我认为你可能对$.运算符之间的区别感到困惑。这里有更多信息。 - hammar
4个回答

18

$的优先级非常低。因此,f x = (+) 5 $ (/) 8 x实际上意味着f x = (+) 5 $ ((/) 8 x)。可以将其重写为:

f x = (+) 5 ( (/) 8 x)
f x = ((+) 5) ( ((/) 8) x)
f x = ((+) 5) .  ( ((/) 8) ) x
f = ((+) 5) . ( (/) 8 )
f = (5+) . (8/)

最后一个表达式是有意义的: f 是两个操作的组合,首先将 8 除以所拥有的数量,然后将 5 加到结果。请记住,g.h 表示“先应用 h,然后将结果应用于 g”。


这实际上是很棒的:所有三个得到赞同的答案展示了问题的不同角度。 - Hi-Angel

13

将λ演算(Haskell是其一个变种)表达式转换为SKI表达式(完全无点的函数,仅使用constK)、idI)和<*>S)),可以使用以下简单规则:

  1. \x -> x 转换为 id
  2. \x -> yy 中不含 x,转换为 const y
  3. \x -> f g 转换为 f' <*> g',其中
    • f'\x -> f 的翻译,
    • g'\x -> g 的翻译。

现在你可能想知道这个 . 是从哪里来的。最后一条翻译有一个特殊情况:如果 f 中没有任何自由出现的 x,那么 \x -> f g 就会被转换为 const f <*> (\x -> g),它等同于 f . (\x -> g)

使用这些规则,我们可以将您的函数转换为:

f = \x -> ((+) 5) (((/) 8) x) = -- by the special-case (.) rule
((+) 5) . (\x -> (((/) 8) x)) = -- by eta-reduction ((\x -> f x) = f)
((+) 5) . ((/) 8)

不必进行Eta-reduction就能完成翻译,但如果没有它,我们会得到一些更混乱的东西。例如,最后一步将产生((+) 5) . ((/) 8) . id


12

可以通过cabal install pointfree安装"pointfree"程序,该程序可以向您展示如何以pointfree样式编写表达式。例如:

$ pointfree "f x = 5 + 8/x"
f = (5 +) . (8 /)

这种转换的解释:

  1. 您可以使用“sections”来表示中缀/运算符函数。 (a +) == \b -> a + b(+ a) == \b -> b + a
  2. . 函数接受第二个参数的结果,该参数是一个单参数函数,并将其应用于第一个参数。

我个人强制自己使用pointfree风格,因为:1.我被迫使我的函数更简单,从而使它们更具可重用性;2.有更大的机会我会真正去重复使用已经编写好的函数,减少代码重复。 - dflemstr
1
请注意,(.)是可结合的,但($)不是,因此(.)在重构方面提供了更大的灵活性。要使用(.),您需要掌握无点格式。此外,单子组合器(monadic combinators)例如liftM2、fmap、>>=和>=>几乎强迫您学习无点格式。 - nponeccop
3
@JFritsch - point free并不总是编写函数的最清晰方式。但是当适用时,它可以帮助你理解函数,因为point free函数只是其他函数的组合。 - Dan Burton
我按照说明安装了 Point Free,但当我输入 $ pointfree 时,它无法识别该命令。我应该将 PATH 指向哪里? - wide_eyed_pupil

8

你非常接近了,让我加上一个$来说明:

f x = (+) 5 $ (/) 8 $ x

很明显,表达式(+) 5是一个以数字为输入并产生数字输出的函数。同样的,表达式(/) 8也是如此。因此,您需要输入任何数字x,首先应用(/) 8“函数”,然后应用(+) 5“函数”。
每当您有一系列由$分隔的函数时,您可以将除最右边的函数之外的所有函数替换为.。也就是说,如果您有a $ b $ c $ d,则等价于a . b . c $ d
f x = (+) 5 . (/) 8 $ x

此时,实际上应该删除$并使用括号代替。

f x = ((+) 5 . (/) 8) x

现在应该很清楚,你可以从两边移除尾随的x
f = (+) 5 . (/) 8

那就是主要思想。如果你有 f x = expr x,你可以将其“eta reduce”到 f = expr。为了生成pointfree代码,你只需要认识到更大的函数是由较小的函数组成的。对于point free代码,部分应用有时是必要的(如在这种情况下,(+) 5(/) 8 被部分应用了)。当你不想思考时,“pointfree”程序非常有帮助;在 #haskell irc 频道上使用这个程序作为插件的Lambdabot,所以你甚至不需要自己安装它;只需要询问即可。
<DanBurton> @pl let f x = 5 + 8 / x in f
<lambdabot> (5 +) . (8 /)

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