"chain" 将加法运算符添加到接受 3 个参数的函数中?

"chain" the addition operator into a function that accepts 3 arguments?

我想编写加法运算符 (+) 来创建这种类型的函数:

Num a => a -> a -> a -> a

就像,相当于:

(\a b c -> a + b + c)

但不必求助于 lambda。


我已经试过了

((+) . (+))

我本以为可以工作,但令人惊讶的是没有。

虽然这引入了一些噪音,但您可以使用 uncurry :: (a -> b -> c) -> (a,b) -> ccurry :: ((a,b) -> c) -> a -> b -> c 将第二个加号的参数临时存储在一个元组中:

curry $ (+) . uncurry (+) :: Num a => a -> a -> a -> a

或者可能更具语义可读性:

curry ((+) . uncurry (+)) :: Num a => a -> a -> a -> a

uncurry 因此接受一个函数(这里是 (+))并将其转换为一个函数:uncurry (+) :: Num a => (a,a) -> a。因此,您已将 (+) 转换为一个 函数,该函数接受一个元组 .

现在我们可以用(.)与第一个(+):

进行合成
(+) . uncurry (+) :: Num a => (a,a) -> (a -> a)

所以现在我们有一个函数接受一个参数(元组 (a,a))并生成一个函数接受 a(第一个 (+) 的第二个操作数)和计算总和。问题当然是我们想摆脱元组。我们可以通过将函数传递给 curry 来实现。这将 tuple-function ((a,a) -> (a -> a)) 转换为单独接受参数的函数 (a -> (a -> (a -> a)))。

http://pointfree.io 给出 \a b c -> a + b + c 的 point-free 版本为 ((+) .) . (+).

非正式地,组合仅适用于 "intuitively" first-order 函数,它既不将函数作为参数也不将 return 函数作为值。 (+) 是一个 higher-order 函数;它采用 Num a => a 类型的值,并且 return 是 Num a => a -> a 类型的函数。当您尝试以天真的方式组合 higher-order 函数时,结果不是您所期望的:

:t (+) . (+)
(+) . (+) :: (Num a, Num (a -> a)) => a -> (a -> a) -> a -> a

考虑两个函数的定义:

(+) :: Num z => z -> z -> z
(.) :: (b -> c) -> (a -> b) -> (a -> c)
f . g = \x -> f (g x)

然后

(+) . (+) == (.) (+) (+)
          == \x -> (+) ((+) x)

由于柯里化,您最终传递了一个函数,而不是数字,作为第一个 (+) 的第一个参数。


那么我们如何从 h a b c = a + b + ch = ((+) .) . (+)?首先将中缀表达式重写为前缀表达式,利用 (+) 是 left-associative.

的事实
\a b c -> a + b + c
     == \a b c -> ((+) a b ) + c
     == \a b c -> (+) ((+) a b) c

接下来,我们交替应用 eta 转换来消除参数和组合以将参数移动到要消除的位置。我试图非常明确地确定用于合成应用程序的函数。

     == \a b -> (+) ((+) a b)      -- eta conversion to eliminate c
     == \a b -> (+) (((+) a) b)    -- parentheses justified by currying
     --          f      g          -- f = (+), g = ((+) a)
     -- \a b ->  f  (   g    b)
     -- \a b -> (f   .  g)   b     -- definition of (.)
     == \a b -> ((+) . ((+) a)) b
     == \a -> (+) . ((+) a)        -- eta conversion to eliminate b
     == \a -> (.) (+) ((+) a)      -- prefix notation
     == \a -> ((.) (+)) ((+) a)    -- parentheses justified by currying
     == \a -> ((+) . )((+) a)      -- back to a section of (.)
     --           f       g        -- f = ((+) .), g = (+)
     -- \a ->     f     (g a)
     -- \a -> (   f   .   g) a     -- definition of (.)
     == \a -> (((+) .) . (+)) a
     == ((+) .) . (+)              -- eta conversion to eliminate a

注意函数组合运算符的签名:

(.) :: (b -> c) -> (a -> b) -> a -> c
          ^            ^          ^
               Functions

它接受 2 个函数,每个函数接受 1 个参数,returns 一个函数接受与第二个函数相同类型的参数,returns 与第一个函数相同类型。

您尝试组合两个 + 的尝试没有成功,因为 + 需要 2 个参数,因此如果没有一些 hackish/creative 解决方法,这是不可能的。

在这一点上,我想说当它不适合问题时强行组合只会让你的生活更加困难。

如果你想对多个数字求和,你可以写一个这样的函数:

sum :: [Int] -> Int
sum nums = foldl (+) 0 nums

或者,由于 nums 出现在定义的后面,它可以完全删除,产生 "point-free" 形式:

sum :: [Int] -> Int
sum = foldl (+) 0

它 reduces/folds + 遍历了一个数字列表。如果您还没有使用过折叠,现在就研究一下。它们是Haskell中实现循环的主要方式之一。当您处理列表或任何其他可迭代对象时,它本质上是 "implicit recursion" 。

定义了上述函数后,您可以像这样使用它:

sum [1, 2  3, 4, 5] 

您需要这个奇怪的运算符 (.).(.),它有时被定义为 .:(想想 3 个点...)

在 ghci

Prelude> let (.:) = (.).(.)
Prelude> let f = (+) .: (+) 
Prelude> f 1 2 3 
> 6

请注意,此运算符也可以定义为 <$$> = fmap . fmap