使用高阶函数组合函数

10

我不明白如何组合多参数函数。 在ghci 7.4.1中,我输入了以下内容:

((*).succ) 3 4
> 16
我不完全理解数学转换,但很明显它与...相同。
(*) (succ 3) 4

但是当我这样做时:

( (\x y z -> x).(\a b -> a*b) ) 2 3 4 5
> 10
( (\x y z -> y).(\a b -> a*b) ) 2 3 4 5
> No instance for (Num (a0 -> t0))

现在我完全迷失了。有人能解释一下发生了什么吗?附:我知道 Haskell 中的所有东西都只有一个参数,但这并没有真正帮助我 :)


3
关于 Haskell 的另一个小细节是,函数应用是左结合的。 - JB.
3
请参考这个stackoverflow答案,了解(.).(.)的细节,它是一个有趣的习语,用于组合多元函数。 - J. Abrahamson
5个回答

14

按照以下方式解决:

(f . g) x = f (g x)
(f . g) x y = f (g x) y -- applying y

然后将f替换为(*),将g替换为succ,并将x和y替换为它们的值:
((*) . succ) 3 4 = (*) (succ 3) 4
                 = (*) 4 4
                 = 16

5

当你组合(\x y z -> x) . (\a b -> a*b)时,你组合的是以下签名的函数:

(\x y z -> x) :: a -> b -> c -> a
(\a b -> a*b) :: Num a => a -> a -> a
(.)的签名是
(.) :: (b -> c) -> (a -> b) -> a -> c

现在,让我们将这些内容整合起来,为这些函数获取一个特定版本的 (.) 签名。
  1. First we specialize the (b -> c) part of (.) signature to a -> b -> c -> a:

    (b -> (z -> x -> b)) -> (a -> b) -> a -> (z -> x -> b)
    

    Get it? The c becomes (z -> x -> b).

  2. Now let's specialize the (a -> b) part of (.) signature to a -> a -> a:

    ((a -> a) -> (z -> x -> (a -> a))) -> (a -> (a -> a)) -> a -> (z -> x -> (a -> a))
    

    The b becomes (a -> a).

  3. Now let's drop the redundant braces:

    ((a -> a) -> z -> x -> a -> a) -> (a -> a -> a) -> a -> z -> x -> a -> a
    
  4. Now here's a session from ghci showing how the signature changes while I consequently apply all the arguments to a function of that signature:

    > let f = undefined :: ((a -> a) -> z -> x -> a -> a) -> (a -> a -> a) -> a -> z -> x -> a -> a
    > :t f (\x y z -> x)
    f (\x y z -> x) :: (a -> a -> a) -> a -> z -> x -> a -> a
    > :t f (\x y z -> x) (\a b -> a*b)
    f (\x y z -> x) (\a b -> a*b) :: Num a => a -> z -> x -> a -> a
    > :t f (\x y z -> x) (\a b -> a*b) 2
    f (\x y z -> x) (\a b -> a*b) 2 :: Num a => z -> x -> a -> a
    > :t f (\x y z -> x) (\a b -> a*b) 2 3
    f (\x y z -> x) (\a b -> a*b) 2 3 :: Num a => x -> a -> a
    > :t f (\x y z -> x) (\a b -> a*b) 2 3 4
    f (\x y z -> x) (\a b -> a*b) 2 3 4 :: Num a => a -> a
    > :t f (\x y z -> x) (\a b -> a*b) 2 3 4 5
    f (\x y z -> x) (\a b -> a*b) 2 3 4 5 :: Num a => a    
    
上面的内容解释了如何运用 ( (\x y z -> x).(\a b -> a*b) ) 2 3 4 5
现在,这是关于如何翻译 ( (\x y z -> y).(\a b -> a*b) ) 2 3 4 5 的说明:
((a -> a) -> z -> x -> z) -> (a -> a -> a) -> a -> z -> x -> z

以下是会话结果:

> let f = undefined :: ((a -> a) -> z -> x -> z) -> (a -> a -> a) -> a -> z -> x -> z
> :t f (\x y z -> x)
f (\x y z -> x) :: (a -> a -> a) -> a -> (a -> a) -> x -> a -> a
> :t f (\x y z -> x) (\a b -> a*b)
f (\x y z -> x) (\a b -> a*b)
  :: Num a => a -> (a -> a) -> x -> a -> a
> :t f (\x y z -> x) (\a b -> a*b) 2
f (\x y z -> x) (\a b -> a*b) 2 :: Num a => (a -> a) -> x -> a -> a
> :t f (\x y z -> x) (\a b -> a*b) 2 3
f (\x y z -> x) (\a b -> a*b) 2 3
  :: (Num a, Num (a -> a)) => x -> a -> a

最后一行解释了你的错误信息。显然,a -> a 没有任何 Num 实例。

3

既然您了解所有内容都是一个参数函数,让我们从这一点开始。请记住,(\x y z -> x) 实际上是 (\x -> (\y z -> x)),而后者实际上是 (\x -> (\y -> (\z -> x))),但为了避免括号过多,让我们在第一步停止。

(f . g) x = f (g x)

因此

((\x -> (\y z -> x)) . (\a b -> a*b)) 2 =
(\x -> (\y z -> x)) ((\a -> (\b -> a*b)) 2) =
(\x -> (\y z -> x)) (\b -> 2*b) =
(\y z -> (\b -> 2*b))

现在请记住第二步并展开 (\y z -> ...):
(\y z -> (\b -> 2*b)) 3 4 =
(\y -> (\z -> (\b -> 2*b))) 3 4 =
   -- \y -> ... given anything, returns a function \z -> ...
(\z -> (\b -> 2*b)) 4 =
   -- \z -> ... given anything, returns a function \b -> ...
(\b -> 2*b)

最终的结果是:

(\b -> 2*b) 5 = 2*5 = 10

如果第一个函数返回 y 而不是 x,那么故事的情节会有所不同:
((\x -> (\y z -> y)) . (\a -> (\b -> a*b))) 2 =
(\x -> (\y z -> y)) ((\a -> (\b -> a*b)) 2) =
(\x -> (\y z -> y)) (\b -> 2*b) =
   -- \x -> ... given anything, returns a function \y z -> ...
(\y z -> y)

所以你得到:

(\y -> (\z -> y)) 3 4 5 =
   -- \y -> ... given anything, returns a function \z -> ...
(\z -> 3) 4 5 =
   -- \z -> ... given anything, returns a constant 3
3 5  -- now still trying to apply 5 to 3

它试图将3视为一个可以接受5的函数。

2
组合子 . 是一个 函数复合 运算符。让我们来看一下它的类型:
(.) :: (b -> c) -> (a -> b) -> a -> c

所以它将第二个函数的结果传递给第一个函数。
在您的例子中,需要考虑的重要事情是,我们可以将*视为一个单参数函数,其结果是一个函数:
(*) :: Num a => a -> (a -> a)

它接收一个数字并返回一个将其参数乘以该数字的函数。(这种方法称为柯里化)。类型运算符 ->与右侧相关联,因此括号是可选的——如果我们将(*)视为返回数字或返回另一个函数的单参数函数,则只存在于我们的思维中。
这有助于我们理解(*) . succ的作用:它将其参数增加一(必须是Enum),然后将其转换为一个将其参数乘以该数字的函数(因此参数也必须是Num)。结果就是
(*) . succ :: (Enum a, Num a) => a -> (a -> a)

再次说明,我们可以把它看做一个有一个参数的函数,或者更方便地将其看作一个有两个参数的函数:它会将第一个参数递增并乘以第二个参数。

0

( (\x y z -> x).(\a b -> a*b) ) 2 3 4 5
  1. 他们首先评估 (\a b -> a*b) 2,得到这样的函数 (\b -> 2*b)
  2. 然后评估 (\x y z -> x) (\b -> 2*b) 3 4,意思是取函数,取3和4并返回函数,类似于这样:((\b -> 2*b) 3 4 -> (\b -> 2*b))
  3. 最后只剩下 (\b -> 2*b) 5,结果为 2*5 = 10

但在...

( (\x y z -> y).(\a b -> a*b) ) 2 3 4 5

第二次评估将产生此结果 ((\b -> 2*b) 3 4 -> 3),剩下的 3 5 导致错误。

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