在静态类型的函数式编程语言中,如标准ML,F#,OCaml和Haskell,函数通常用参数分隔,并且只用空格从函数名中写出:
let add a b =
a + b
这里的类型是“int -> (int -> int)
”,即一个接受int的函数,返回一个转到的函数和int,最后返回一个int。因此,currying成为可能。
也可以定义一个以元组作为参数的类似函数:
let add(a, b) =
a + b
在这种情况下,类型变为“(int * int) -> int
”。
从语言设计的角度来看,有没有理由不能简单地在类型代数中识别这两种类型模式?换句话说,使“(a * b) - > c”减少到“a - >(b - > c)”,允许两种变体同样容易地进行曲解。
我认为当我提到的四种语言被设计出来时,这个问题肯定会出现。那么有谁知道任何理由或研究表明为什么所有这四种语言都选择不“统一”这两种类型模式?
答案 0 :(得分:7)
我认为今天的共识是通过currying(a -> b -> c
形式)处理多个参数并保留元组,以便在你真正想要元组类型的值时(在列表中等等) )。自标准ML以来,每个静态类型的函数语言都遵循这种共识,(纯粹作为惯例)将元组用于带有多个参数的函数。
为什么会这样?标准ML是这些语言中最古老的,当人们第一次编写ML编译器时,不知道如何有效地处理curried参数。 (问题的根源是任何 curried函数可以部分地应用你尚未见过的其他代码,并且你必须用它编译它考虑到这种可能性。)自设计标准ML以来,编译器已得到改进,您可以在excellent paper by Simon Marlow and Simon Peyton Jones中了解最新技术。
LISP是它们中最古老的函数式语言,它具有一种具体的语法,它对于currying和部分应用程序非常不友好。 Hrmph。
答案 1 :(得分:4)
至少有一个不混淆curried和uncurried函数类型的原因是当元组用作返回值时会非常混乱(这些类型语言中的一种方便的方法是返回多个值)。在混合型系统中,功能如何保持良好的可组合性? a -> (a * a)
也会转换为a -> a -> a
吗?如果是,(a * a) -> a
和a -> (a * a)
是同一类型吗?如果没有,您将如何使用a -> (a * a)
撰写(a * a) -> a
?
您提出了一个有趣的解决方案来解决构图问题。但是,我觉得它并不令人满意,因为它与部分应用程序不能很好地融合,这实际上是curry函数的一个关键方便。让我试着说明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)打开了包装适用的a& b参数?这不是意图......无论如何,推理变得复杂。
简而言之,我认为在(1)保留旧系统下有效的代码语义和(2)为混合系统提供合理的直觉和算法时,很难将curried和uncurried函数混淆。不过,这是一个有趣的问题。
答案 2 :(得分: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
因此uncurry add1
与add2
相同,curry add2
与add1
相同。我想其他语言也可能有类似的东西。有什么更大的收获可以自动调整元组中定义的每个函数? (因为那是你似乎要问的。)
答案 3 :(得分:2)
这些语言中不存在元组只是用作函数参数。它们是表示结构化数据的非常方便的方式,例如2D点(int * int
),列表元素('a * 'a list
)或树节点('a * 'a tree * 'a tree
)。还要记住,结构只是元组的语法糖。即,
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
答案 4 :(得分:2)
根据纳米的好答案扩大评论:
因此假设一个类型为'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元组的小函数,并列出两个元素: / p>
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)
类型的函数,最终会返回一个列表,其头部将是元组{{} 1}?为了实现这一点,我在对namin的回答中提出一个评论,即在类型系统中需要一个额外的规则,即实际参数与形式参数的绑定是“尽可能最大”,即系统应该尝试非部分绑定首先,如果完全绑定无法实现,则只尝试部分绑定。在我们的示例中,这意味着我们得到值(10, 15)
,因为在这种情况下可以进行完全绑定。
这种“尽可能”的概念本身就没有意义吗?我不知道,但我不能立即看到它会有原因。
当然,如果你希望对最后一个代码片段进行第二次解释,那么你需要跳过一个额外的环(通常是一个匿名函数)才能得到它。 / p>