编写函数签名

Compose function signature

我读到 g :: A -> B 和 f :: B -> C 的组合,发音为“f composed of g”,导致另一个函数(箭头)从 A -> C. 这可以更正式地表达为

f • g = f(g) = compose :: (B -> C) -> (A -> B) -> (A -> C)

上面的组合是否也可以定义如下?请说清楚。 在这种情况下,compose 函数采用相同的两个函数 f 和 g 以及 return 来自 A -> C 的新函数。

f • g = f(g) = compose :: ((B -> C), (A -> B)) -> (A -> C)

首先比较常用的是空心圆:f ∘ g.

其次,它的发音更恰当"f composed with g"。 ("f composed of g" 听起来像 f 是由 g 组成的,而不是一个由两者组成的新函数。)

最后,这两种类型在本质上是相同的,不同之处仅在于您期望将函数传递给 compose 函数的方式。第一个定义了完全柯里化函数的类型,这样 compose 接受一个函数作为参数,return 是一个接受第二个函数作为参数的新函数,return 是组成。这意味着使用 f :: B -> Cg :: A -> B,您可以定义(使用 Haskell 语法)

compose :: (B -> C) -> (A -> B) -> (A -> C)
compose f g = \x -> f (g x)

或未柯里化版本

compose' :: ((B -> C), (A -> B)) -> (A -> C)
compose' (f, g) = \x -> f (g x)

无论哪种方式,return 值都是相同的;唯一的区别在于参数的传递方式。你可以写 h = compose f g 或者你可以写 h = compose' (f, g).

首先我们需要做对一些事情:

  • f ○ gf(g).

    的意思完全不同
    • 前者是一个函数,给定一个参数x,首先将它提供给g然后将结果传递给f,并输出最终结果,即f(g(x)).
    • OTOH,f(g) 意味着您立即将函数 f 应用于 value g,无需等待任何参数。 (g 恰好有一个函数类型,但在函数式语言中,函数可以像任何其他值/参数一样传递。

    除非您处理的是一些非常古怪的多态函数,否则其中一个将是错误类型的。例如,一个类型正确的组合可能是

    sqrt ○ abs :: Double -> Double
    

    而类型良好的 应用程序 可能是(至少在 Haskell 中)

    map(sqrt) :: [Double] -> [Double]
    

    我假设您在下文中谈论的是 f ○ g.

  • 必须为函数本身提供类型签名,而不是为应用于某些参数的函数提供类型签名。这是很多人完全错误的地方:在 f(x) 中,你有一个函数 f 和一个参数 x。但是 f(x) 不是 函数,它只是将函数应用于某个值的结果!所以,你不应该写像 f ○ g :: ... 这样的东西(除非你实际上只是在谈论 从组合中得到 的类型)。最好只写 ○ :: ...(或者 Haskell、(○) :: ...)。

  • 函数箭头不是关联的。大多数数学家可能甚至不知道 X -> Y -> Z 是什么意思。它在 Haskell 等语言中的含义实际上可能有些令人惊讶:

    X -> Y -> Z  ≡  X -> (Y -> Z)
    

即这是函数的类型,它首先只接受 X 类型的参数。结果将再次是一个函数,但只接受类型为 Y 的参数。如果您愿意,此函数将具有已经内置的 X 值(在所谓的 closure 中,除非编译器将其优化掉)。也给它 Y 值将允许函数实际完成它的工作并最终产生 Z 结果。

在这一点上你已经有了答案,几乎:实际上签名 X -> Y -> Z(X, Y) -> Z 本质上是等价的。这个重写的过程叫做currying.

特别回答你的问题:大多数语言通常不做任何柯里化,所以签名 ((B -> C), (A -> B)) -> (A -> C) 实际上更正确。它对应一个你可以调用的函数 as

   compose(f,g)

OTOH,curried 签名 (B -> C) -> (A -> B) -> (A -> C) 意味着您需要一个一个地输入参数:

   compose(f)(g)

只有像 Haskell 这样的语言才是标准样式,但你不需要那里的括号:以下所有内容在 Haskell

中的解析相同
   compose(f)(g)
   compose f g
   (compose) f g
   (.) f g
   f . g

其中 . 实际上是 composition operator,正如您从文档中看到的那样,其类型为

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

由于您用 Javascript 标记了您的问题,这里是从 Javascript 的角度回答的。

假设我正确理解了您的签名,您希望按如下方式调整组合函数:(f, g) => x => f(g(x));。当然,这行得通,但你失去了灵活性并获得了嗯,什么都没有。

原来的 curry 函数是以 curried 形式定义的,这意味着它总是需要一个参数。如果整个代码中的每个函数都只需要一个参数,那么就没有更多的参数(好吧,在大多数情况下)。它被抽象掉了。柯里化有利于函数组合,因为函数总是 return 单个值。咖喱函数就像积木一样。您几乎可以用任何方式将它们放在一起:

    const comp = f => g => x => f(g(x)),
     comp2 = comp(comp)(comp),
     add = y => x => x + y,
     inc = x => x + 1,
     sqr = x => x * x;
    
    console.log(comp(sqr)(inc)(2)); // 9
    console.log(comp(add)(sqr)(2)(3)); // 7
    console.log(comp2(sqr)(add)(2)(3)); // 25

正如您所看到的,只有在后一种情况下,我们才必须考虑数量。

只有始终如一地应用于代码库的每个功能,柯里化才能发挥其优势,因为它具有系统效应。