Haskell中的无点风格

17

我有这段代码,希望能够变成点分式;

(\k t -> chr $ a + flip mod 26 (ord k + ord t -2*a))

我该怎么做呢?

此外,除了“考虑这个并想出一些东西”之外,是否有一些点分式样式的通用规则?


4
为什么您希望将其点无形式表示? - yfeldblum
1
因为能够编写无点代码似乎是一名优秀的Haskell程序员的特征之一。 - Igor
10
有时候,点无代码比非点无代码更清晰明了,这时使用点无风格是个好主意。但是这不是那种情况。 - dave4420
4
你可以像http://en.wikipedia.org/wiki/Combinatory_logic#Completeness_of_the_S-K_basis所描述的那样进行SKI分解,其中S=Control.Monad.ap,K=const,I=id。但这与Haskeller所做的使代码无参考点的方法相去甚远。我从分析自己的代码并尝试将其变得更美观、学习新的组合子(如Control.Arrow.(***, &&&)、applicative符号、Data.Function.on等)中学到了很多。 - luqui
点无代码通常是非常疯狂天才的标志。点无代码有时是一个优秀的Haskell程序员的标志。我不认为这是一个可以简单地转换成点无代码的情况。 - yfeldblum
以点无关的方式表达函数是否允许一些编译优化,否则您将无法获得这些优化?我想我在某个地方、某个时候读到过这样的内容。 - wide_eyed_pupil
5个回答

52
将一个函数转换
func x y z = (some expression in x, y and z)

当我将函数转化为点无形式时,我通常会尝试按照对最后一个参数z的处理方式来编写函数,如下:

func x y z = (some function pipeline built using x and y) z

然后我可以消去z,得到

func x y = (some function pipeline built using x and y)

然后重复以上过程,对 y 和 x 进行操作,最终得到用点运算符表示的 func。在这个过程中需要注意的一个关键转换是:
    f z = foo $ bar z    -- or f z = foo (bar z)
<=> f z = foo . bar $ z
<=> f   = foo . bar

还需记住,使用部分求值技术,你可以“分离”函数的最后一个参数:

foo $ bar x y == foo . bar x $ y    -- foo applied to ((bar x) applied to y)

针对您的特定函数,请考虑以下流程,即kt经历的过程:
  1. 将其分别应用ord
  2. 添加结果
  3. 减去2*a
  4. 将结果模26
  5. 添加a
  6. 应用chr
因此,我们首先尝试简化如下:
func k t = chr . (+a) . (`mod` 26) . subtract (2*a) $ ord k + ord t

请注意,您可以通过在mod部分上使用一个部分来避免flip,并且在使用-的部分中,Haskell会变得混乱,因此有一个subtract函数(它们与写负数的语法冲突:(-2)表示负2,并且与subtract 2不同)。
在这个函数中,ord k + ord t非常适合使用Data.Function.onlink)。 这个有用的组合器让我们用应用于kt的函数替换ord k + ord t
func k t = chr . (+a) . (`mod` 26) . subtract (2*a) $ ((+) `on` ord) k t

我们现在非常接近拥有

func k t = (function pipeline) k t

因此

func = (function pipeline)

不幸的是,当需要将一个二元函数与一系列一元函数组合时,Haskell 有点混乱,但是有一个技巧(我会看看是否能找到一个好的参考),我们最终得到:

import Data.Function (on)

func = ((chr . (+a) . (`mod` 26) . subtract (2*a)) .) . ((+) `on` ord)

这几乎是一个漂亮整齐的无参函数流水线,除了那个丑陋的组合技巧。通过定义在此页面评论中建议的.:运算符,这个技巧变得更加简洁:

import Data.Function (on)

(.:) = (.).(.)

func = (chr . (+a) . (`mod` 26) . subtract (2*a)) .: ((+) `on` ord)

为了更加完善,您可以添加一些帮助函数来将字母与整数之间的转换与凯撒密码算法分开。例如:letterToInt = subtract a . ord

减去(2a)== (+(-2a))。 - Will Ness

10

除了“思考并得出一些东西”之外,是否有一些关于“无参式风格”的普遍规则呢?

你总是可以使用 lambdabot 中的“pl”工具来作弊(可以通过在 Freenode 上进入 #haskell 或者使用例如“ghci on acid”)。输入您的代码后,pl 将给出以下结果:

((chr . (a +) . flip mod 26) .) . flip flip (2 * a) . ((-) .) . (. ord) . (+) . ord

如果你问我,这并没有真正改进。


3
我假设你使用点无意义编程的目的是使代码更简洁、更易读。因此,我认为还应该进行一些其他的重构工作,以达到简化的效果,这样就更容易删除变量了。
(\k t -> chr $ a + flip mod 26 (ord k + ord t - 2*a))

首先,flip是不必要的:
(\k t -> chr $ a + (ord k + ord t - 2*a) `mod` 26)

接下来,我会使用“名称取代”(name and conquer)来分解出一个可以独立使用的子函数:
encode_characters k t = chr $ encode (ord k) (ord t)
encode x y = (x + y - 2*a) `mod` 26 + a

我还给第一个表达式起了一个名字,以使其更清晰、可重用。使用@Nefrubyr的技巧可以轻松地将encode_characters变为无参数形式:

encode_characters = chr . encode `on` ord

关于第二个表达式,我无法提供比其他答案中显示的更易读的表格,并且它们都不如逐点形式易读。因此,建议在这一点停止重构,并欣赏最终代码的清晰度和可重用性。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
PS:作为一个练习,根据问题的上下文,通过对函数接口(以什么形式传递数据到函数中)进行一些轻微的修改,可能会通过概括问题而获得更多的简化。
A. 实现并简化函数 encode_n_characters :: [Char] -> Char ,其中 encode_characters k t = encode_n_characters [k,t]。结果是否比专门的双参数函数更简单?
B. 通过 encode' (x + y) = encode x y 定义 encode' 函数,并重新实现 encode_characters。这两个函数中的哪一个变得更简单?整体实现是否更简单?encode' 是否比 encode 更可重用?

3

将表达式转换成点无风格的技巧一定存在。虽然我不是专家,但以下是一些提示。

首先,您需要将函数参数隔离在表达式的最右侧术语中。您的主要工具将是flip$,使用以下规则:

f a b ==> flip f b a
f (g a) ==> f $ g a

这里的 fg 是函数,ab 是表达式。因此,我们可以开始:

(\k t -> chr $ a + flip mod 26 (ord k + ord t -2*a))
-- replace parens with ($)
(\k t -> chr $ (a +) . flip mod 26 $ ord k + ord t - 2*a)
-- prefix and flip (-)
(\k t -> chr $ (a +) . flip mod 26 $ flip (-) (2*a) $ ord k + ord t)
-- prefix (+)
(\k t -> chr $ (a +) . flip mod 26 $ flip (-) (2*a) $ (+) (ord k) (ord t))

现在我们需要将t放到右边。为了做到这一点,使用以下规则:
f (g a) ==> (f . g) a

因此:

-- pull the t out on the rhs
(\k t -> chr $ (a +) . flip mod 26 $ flip (-) (2*a) $ ((+) (ord k) . ord) t)
-- flip (.) (using a section)
(\k t -> chr $ (a +) . flip mod 26 $ flip (-) (2*a) $ ((. ord) $ (+) (ord k)) t)
-- pull the k out
(\k t -> chr $ (a +) . flip mod 26 $ flip (-) (2*a) $ ((. ord) . ((+) . ord)) k t)

现在,我们需要将 kt 左侧的所有内容转换为一个大函数项,以便我们得到一个形如 (\k t -> f k t) 的表达式。这时事情变得有点费脑子。首先,注意到最后一个 $ 之前的所有项都是带有单个参数的函数,因此我们可以将它们组合起来:
(\k t -> chr . (a +) . flip mod 26 . flip (-) (2*a) $ ((. ord) . ((+) . ord)) k t)

现在,我们有一个类型为Char -> Char -> Int的函数,我们想要将其与一个类型为Int -> Char的函数组合,得到一个类型为Char -> Char -> Char的函数。我们可以使用(非常奇怪的)规则来实现这一点。
f (g a b) ==> ((f .) . g) a b

这给我们带来了:
(\k t -> (((chr . (a +) . flip mod 26 . flip (-) (2*a)) .) . ((. ord) . ((+) . ord))) k t)

现在我们可以直接应用β规约:
((chr . (a +) . flip mod 26) .) . (flip flip (2*a) . ((-) . ) . ((. ord) . (+) .ord))

使用 ->,Monad、Applicative 或 Arrow 的实例也是很不错的技巧。 - ephemient
f (g a) ==> f $ g a 并没有真正帮助到你。右边仍然是 f $ (g a)。你需要的是函数组合。f (g a) 可以写成 (f . g) a - Prateek

0

还没有发布,因为我正在手动尝试解决问题,而不是问lambdabot……一个编辑即将到来。 - David V.
哎呀,我想到了 let f1 = \a -> (chr .) . ((a+).).((flip mod 26).).(.ord).(+).((-) (2*a)) . ord 这个函数,但它给出了错误的结果,而且我现在也不想调试它 :) - David V.

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