7

在静态类型的函数式编程语言中,如标准 ML、F#、OCaml 和 Haskell,函数通常会使用彼此分隔的参数和函数名称简单地通过空格来编写:

let add a b =
    a + b

这里的类型是“ int -> (int -> int)”,即一个函数,它接受一个 int 并返回一个函数,该函数又接受一个 int 并最终返回一个 int。因此,柯里化成为可能。

也可以定义一个以元组作为参数的类似函数:

let add(a, b) =
    a + b

(int * int) -> int在这种情况下,类型变为“ ”。

从语言设计的角度来看,有什么理由不能简单地识别类型代数中的这两种类型模式?换句话说,使得“(a * b) -> c”简化为“a -> (b -> c)”,从而允许两个变体同样容易地被柯里化。

我想这个问题一定是在设计我提到的四种语言时出现的。那么有没有人知道任何原因或研究表明为什么所有这四种语言都选择不“统一”这两种类型模式?

4

5 回答 5

7

我认为今天的共识是通过柯里化(表单)来处理多个参数a -> b -> c,并在您真正想要元组类型的值(在列表等中)时保留元组。自标准 ML 以来,每种静态类型的函数式语言都尊重这种共识,标准 ML(纯粹作为惯例)将元组用于接受多个参数的函数。

为什么会这样?标准 ML 是这些语言中最古老的,当人们第一次编写 ML 编译器时,还不知道如何有效地处理柯里化参数。(问题的根源在于,任何柯里化函数都可能被您尚未见过的其他代码部分应用,并且您必须在考虑这种可能性的情况下对其进行编译。)由于设计了标准 ML,编译器具有改进了,您可以在Simon Marlow 和 Simon Peyton Jones 的一篇优秀论文中了解最新技术。

LISP 是所有语言中最古老的函数式语言,其具体语法对柯里化和部分应用极为不友好。哼。

于 2009-01-02T02:57:59.157 回答
4

不将 curried 和 uncurried 函数类型混为一谈的至少一个原因是,当元组用作返回值时会非常混乱(这些类型化语言中返回多个值的一种方便方式)。在混合类型系统中,函数如何保持良好的可组合性?a -> (a * a)还会a -> a -> a变身?如果是,(a * a) -> aa -> (a * a)是同一类型吗?a -> (a * a)如果没有,你会如何作曲(a * a) -> a

你为作文问题提出了一个有趣的解决方案。但是,我觉得它并不令人满意,因为它不能很好地与部分应用程序相结合,这确实是柯里化函数的一个关键便利。让我尝试在 Haskell 中说明问题:

apply f a b = f a b
vecSum (a1,a2) (b1,b2) = (a1+b1,a2+b2)

现在,也许您的解决方案可以理解map (vecSum (1,1)) [(0,1)],但是等价的map (apply vecSum (1,1)) [(0,1)]呢?变得复杂了!您最完整的解包是否意味着 (1,1) 使用 apply 的 a 和 b 参数解包?这不是意图......无论如何,推理变得复杂。

简而言之,我认为在 (1) 保留旧系统下有效代码的语义和 (2) 为合并后的系统提供合理的直觉和算法的同时,将 curried 和 uncurried 函数混为一谈是非常困难的。不过,这是一个有趣的问题。

于 2009-01-02T01:04:16.720 回答
2

可能是因为将元组作为单独的类型很有用,并且将不同的类型分开很好?

至少在 Haskell 中,从一种形式转换到另一种形式非常容易:

Prelude> let add1 a b = a+b
Prelude> let add2 (a,b) = a+b
Prelude> :t (uncurry add1)
(uncurry add1) :: (Num a) => (a, a) -> a
Prelude> :t (curry add2)
(curry add2) :: (Num a) => a -> a -> a

souncurry add1与 相同,add2curry add2相同add1。我想类似的事情在其他语言中也是可能的。自动柯里化在元组上定义的每个函数会有什么更大的收获?(因为这似乎是你要问的。)

于 2009-01-02T00:54:33.260 回答
2

元组在这些语言中不存在只是为了用作函数参数。它们是表示结构化数据的一种非常方便的方式,例如,2D 点 ( int * int)、列表元素 ( 'a * 'a list) 或树节点 ( 'a * 'a tree * 'a tree)。还要记住,结构只是元组的语法糖。IE,

type info = 
  { name : string;
    age : int;
    address : string; }

是一种处理(string * int * string)元组的便捷方式。

在编程语言中没有需要元组的根本原因(您可以在 lambda 演算中构建元组,就像您可以使用布尔值和整数一样 - 实际上使用 currying*),但它们方便且有用。

*例如,

tuple a b = λf.f a b
fst x     = x (λa.λb.a)
snd x     = x (λa.λb.b)
curry f   = λa.λb.f (λg.g a b)
uncurry f = λx.x f
于 2009-01-02T01:23:44.620 回答
2

扩展 namin 的好答案下的评论:

所以假设一个函数的类型为'a -> ('a * 'a)

let gimme_tuple(a : int) =
    (a*2, a*3)

然后假设一个具有类型的函数('a * 'a) -> 'b

let add(a : int, b) =
    a + b

然后组合(假设我建议的合并)不会造成任何问题,据我所知:

let foo = add(gimme_tuple(5))
// foo gets value 5*2 + 5*3 = 25

但是你可以设想一个多态函数来代替add最后一个代码片段,例如一个小函数,它只接受一个 2 元组并列出两个元素:

let gimme_list(a, b) =
    [a, b]

这将具有类型('a * 'a) -> ('a list)。现在的作曲将是有问题的。考虑:

let bar = gimme_list(gimme_tuple(5))

bar现在会有值[10, 15] : int list还是bar类型的函数(int * int) -> ((int * int) list),它最终会返回一个列表,其头部是元组(10, 15)?为此,我在对 namin 的回答的评论中提出,在类型系统中需要一个额外的规则,即实际参数与形式参数的绑定是“尽可能完整的”,即系统应该尝试非部分绑定首先并且仅在无法实现完全绑定时才尝试部分绑定。在我们的示例中,这意味着我们获得了值[10, 15],因为在这种情况下可以进行完全绑定。

这种“尽可能充分”的概念本身就没有意义吗?我不知道,但我不能立即看到它的原因。

一个问题当然是,如果您想要对最后一个代码片段进行第二种解释,那么您需要跳过一个额外的圈(通常是一个匿名函数)来获得它。

于 2009-01-02T02:05:21.557 回答