类型FP:元组参数和可曲解参数

时间:2022-07-14 22:03:06

In statically typed functional programming languages, like Standard ML, F#, OCaml and Haskell, a function will usually be written with the parameters separated from each other and from the function name simply by whitespace:

在静态类型的函数式编程语言中,如标准ML,F#,OCaml和Haskell,函数通常用参数相互分离,并且只用空格从函数名中编写:

let add a b =
    a + b

The type here being "int -> (int -> int)", i.e. a function that takes an int and returns a function which its turn takes and int and which finally returns an int. Thus currying becomes possible.

这里的类型是“int - >(int - > int)”,即一个接受int的函数,返回一个转到的函数和int,最后返回一个int。因此,成为可能。

It's also possible to define a similar function that takes a tuple as an argument:

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

let add(a, b) =
    a + b

The type becomes "(int * int) -> int" in this case.

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

From the point of view of language design, is there any reason why one could not simply identify these two type patterns in the type algebra? In other words, so that "(a * b) -> c" reduces to "a -> (b -> c)", allowing both variants to be curried with equal ease.

从语言设计的角度来看,有没有理由不能简单地在类型代数中识别这两种类型模式?换句话说,使“(a * b) - > c”减少到“a - >(b - > c)”,允许两个变体同样容易地变成曲线。

I assume this question must have cropped up when languages like the four I mentioned were designed. So does anyone know any reason or research indicating why all four of these languages chose not to "unify" these two type patterns?

我认为当我提到的四种语言被设计出来时,这个问题肯定会出现。那么有谁知道任何理由或研究表明为什么所有这四种语言都选择不“统一”这两种类型模式?

5 个解决方案

#1


I think the consensus today is to handle multiple arguments by currying (the a -> b -> c form) and to reserve tuples for when you genuinely want values of tuple type (in lists and so on). This consensus is respected by every statically typed functional language since Standard ML, which (purely as a matter of convention) uses tuples for functions that take multiple arguments.

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

Why is this so? Standard ML is the oldest of these languages, and when people were first writing ML compilers, it was not known how to handle curried arguments efficiently. (At the root of the problem is the fact that any curried function could be partially applied by some other code you haven't seen yet, and you have to compile it with that possibility in mind.) Since Standard ML was designed, compilers have improved, and you can read about the state of the art in an excellent paper by Simon Marlow and Simon Peyton Jones.

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

LISP, which is the oldest functional language of them all, has a concrete syntax which is extremely unfriendly to currying and partial application. Hrmph.

LISP是它们中最古老的函数式语言,它具有一种具体的语法,对于currying和部分应用程序非常不友好。 Hrmph。

#2


At least one reason not to conflate curried and uncurried functional types is that it would be very confusing when tuples are used as returned values (a convenient way in these typed languages to return multiple values). In the conflated type system, how can function remain nicely composable? Would a -> (a * a) also transform to a -> a -> a? If so, are (a * a) -> a and a -> (a * a) the same type? If not, how would you compose a -> (a * a) with (a * a) -> a?

至少有一个不混淆curried和uncurried函数类型的原因是当元组用作返回值时会非常混乱(这些类型语言中返回多个值的便捷方式)。在混合型系统中,功能如何保持良好的可组合性? a - >(a * a)也会转换为 - > a - > a?如果是这样,是(a * a) - > a和a - >(a * a)是同一类型吗?如果没有,你会如何用(a * a) - > a组成一个 - >(a * a)?

You propose an interesting solution to the composition issue. However, I don't find it satisfying because it doesn't mesh well with partial application, which is really a key convenience of curried functions. Let me attempt to illustrate the problem in Haskell:

您提出了一个有趣的组合问题解决方案。但是,我觉得它并不令人满意,因为它与部分应用程序不能很好地融合,这实际上是curry函数的关键方便。让我试着说明Haskell中的问题:

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

Now, perhaps your solution could understand map (vecSum (1,1)) [(0,1)], but what about the equivalent map (apply vecSum (1,1)) [(0,1)]? It becomes complicated! Does your fullest unpacking mean that the (1,1) is unpacked with apply's a & b arguments? That's not the intent... and in any case, reasoning becomes complicated.

现在,也许你的解决方案可以理解map(vecSum(1,1))[(0,1)],但是等效映射怎么样(应用vecSum(1,1))[(0,1)]?变得复杂了!你最充分的解包是否意味着(1,1)用apply a和b参数解包?这不是意图......无论如何,推理变得复杂。

In short, I think it would be very hard to conflate curried and uncurried functions while (1) preserving the semantics of code valid under the old system and (2) providing a reasonable intuition and algorithm for the conflated system. It's an interesting problem, though.

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

#3


Possibly because it is useful to have a tuple as a separate type, and it is good to keep different types separate?

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

In Haskell at least, it is quite easy to go from one form to the other:

至少在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

so uncurry add1 is same as add2 and curry add2 is the same as add1. I guess similar things are possible in the other languages as well. What greater gains would there be to automatically currying every function defined on a tuple? (Since that's what you seem to be asking.)

所以uncurry add1与add2相同,而curry add2与add1相同。我想其他语言也可能有类似的东西。有什么更大的收获可以自动调整元组中定义的每个函数? (因为那是你似乎要问的。)

#4


Expanding on the comments under namin's good answer:

根据纳米的好答案扩大评论:

So assume a function which has type 'a -> ('a * 'a):

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

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

Then assume a function which has type ('a * 'a) -> 'b:

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

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

Then composition (assuming the conflation that I propose) wouldn't pose any problem as far as I can see:

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

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

But then you could conceive of a polymorphic function that takes the place of add in the last code snippet, for instance a little function that just takes a 2-tuple and makes a list of the two elements:

但是你可以设想一个多态函数,它取代了最后一个代码片段中的add,例如一个只占用2元组的小函数,并列出了两个元素:

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

This would have the type ('a * 'a) -> ('a list). Composition now would be problematic. Consider:

这将具有类型('a *'a) - >('列表)。现在的成分会有问题。考虑:

let bar = gimme_list(gimme_tuple(5))

Would bar now have the value [10, 15] : int list or would bar be a function of type (int * int) -> ((int * int) list), which would eventually return a list whose head would be the tuple (10, 15)? For this to work, I posited in a comment to namin's answer that one would need an additional rule in the type system that the binding of actual to formal parameters be the "fullest possible", i.e. that the system should attempt a non-partial binding first and only try a partial binding if a full binding is unattainable. In our example, it would mean that we get the value [10, 15] because a full binding is possible in this case.

bar现在有值[10,15]:int list或者bar是类型(int * int) - >((int * int)list)的函数,它最终将返回一个头部为元组的列表(10,15)?为了实现这个目的,我在对namin的答案的评论中提出,在类型系统中需要一个额外的规则,即实际参数与形式参数的绑定是“最可能的”,即系统应该尝试非部分绑定首先,如果完全绑定无法实现,则只尝试部分绑定。在我们的例子中,这意味着我们得到值[10,15],因为在这种情况下可以完全绑定。

Is such a concept of "fullest possible" inherently meaningless? I don't know, but I can't immediately see a reason it would be.

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

One problem is of course if you want the second interpretation of the last code snippet, then you'd need to go jump through an extra hoop (typically an anonymous function) to get that.

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

#5


Tuples are not present in these languages simply to be used as function parameters. They are a really convenient way to represent structured data, e.g., a 2D point (int * int), a list element ('a * 'a list), or a tree node ('a * 'a tree * 'a tree). Remember also that structures are just syntactic sugar for tuples. I.e.,

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

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

is a convenient way of handling a (string * int * string) tuple.

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

There's no fundamental reason you need tuples in a programming language (you can build up tuples in the lambda calculus, just as you can booleans and integers—indeed using currying*), but they are convenient and useful.

在编程语言中你需要元组没有根本原因(你可以在lambda演算中建立元组,就像你可以使用布尔和整数一样 - 确实使用currying *),但它们既方便又有用。

* E.g.,

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

#1


I think the consensus today is to handle multiple arguments by currying (the a -> b -> c form) and to reserve tuples for when you genuinely want values of tuple type (in lists and so on). This consensus is respected by every statically typed functional language since Standard ML, which (purely as a matter of convention) uses tuples for functions that take multiple arguments.

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

Why is this so? Standard ML is the oldest of these languages, and when people were first writing ML compilers, it was not known how to handle curried arguments efficiently. (At the root of the problem is the fact that any curried function could be partially applied by some other code you haven't seen yet, and you have to compile it with that possibility in mind.) Since Standard ML was designed, compilers have improved, and you can read about the state of the art in an excellent paper by Simon Marlow and Simon Peyton Jones.

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

LISP, which is the oldest functional language of them all, has a concrete syntax which is extremely unfriendly to currying and partial application. Hrmph.

LISP是它们中最古老的函数式语言,它具有一种具体的语法,对于currying和部分应用程序非常不友好。 Hrmph。

#2


At least one reason not to conflate curried and uncurried functional types is that it would be very confusing when tuples are used as returned values (a convenient way in these typed languages to return multiple values). In the conflated type system, how can function remain nicely composable? Would a -> (a * a) also transform to a -> a -> a? If so, are (a * a) -> a and a -> (a * a) the same type? If not, how would you compose a -> (a * a) with (a * a) -> a?

至少有一个不混淆curried和uncurried函数类型的原因是当元组用作返回值时会非常混乱(这些类型语言中返回多个值的便捷方式)。在混合型系统中,功能如何保持良好的可组合性? a - >(a * a)也会转换为 - > a - > a?如果是这样,是(a * a) - > a和a - >(a * a)是同一类型吗?如果没有,你会如何用(a * a) - > a组成一个 - >(a * a)?

You propose an interesting solution to the composition issue. However, I don't find it satisfying because it doesn't mesh well with partial application, which is really a key convenience of curried functions. Let me attempt to illustrate the problem in Haskell:

您提出了一个有趣的组合问题解决方案。但是,我觉得它并不令人满意,因为它与部分应用程序不能很好地融合,这实际上是curry函数的关键方便。让我试着说明Haskell中的问题:

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

Now, perhaps your solution could understand map (vecSum (1,1)) [(0,1)], but what about the equivalent map (apply vecSum (1,1)) [(0,1)]? It becomes complicated! Does your fullest unpacking mean that the (1,1) is unpacked with apply's a & b arguments? That's not the intent... and in any case, reasoning becomes complicated.

现在,也许你的解决方案可以理解map(vecSum(1,1))[(0,1)],但是等效映射怎么样(应用vecSum(1,1))[(0,1)]?变得复杂了!你最充分的解包是否意味着(1,1)用apply a和b参数解包?这不是意图......无论如何,推理变得复杂。

In short, I think it would be very hard to conflate curried and uncurried functions while (1) preserving the semantics of code valid under the old system and (2) providing a reasonable intuition and algorithm for the conflated system. It's an interesting problem, though.

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

#3


Possibly because it is useful to have a tuple as a separate type, and it is good to keep different types separate?

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

In Haskell at least, it is quite easy to go from one form to the other:

至少在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

so uncurry add1 is same as add2 and curry add2 is the same as add1. I guess similar things are possible in the other languages as well. What greater gains would there be to automatically currying every function defined on a tuple? (Since that's what you seem to be asking.)

所以uncurry add1与add2相同,而curry add2与add1相同。我想其他语言也可能有类似的东西。有什么更大的收获可以自动调整元组中定义的每个函数? (因为那是你似乎要问的。)

#4


Expanding on the comments under namin's good answer:

根据纳米的好答案扩大评论:

So assume a function which has type 'a -> ('a * 'a):

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

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

Then assume a function which has type ('a * 'a) -> 'b:

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

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

Then composition (assuming the conflation that I propose) wouldn't pose any problem as far as I can see:

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

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

But then you could conceive of a polymorphic function that takes the place of add in the last code snippet, for instance a little function that just takes a 2-tuple and makes a list of the two elements:

但是你可以设想一个多态函数,它取代了最后一个代码片段中的add,例如一个只占用2元组的小函数,并列出了两个元素:

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

This would have the type ('a * 'a) -> ('a list). Composition now would be problematic. Consider:

这将具有类型('a *'a) - >('列表)。现在的成分会有问题。考虑:

let bar = gimme_list(gimme_tuple(5))

Would bar now have the value [10, 15] : int list or would bar be a function of type (int * int) -> ((int * int) list), which would eventually return a list whose head would be the tuple (10, 15)? For this to work, I posited in a comment to namin's answer that one would need an additional rule in the type system that the binding of actual to formal parameters be the "fullest possible", i.e. that the system should attempt a non-partial binding first and only try a partial binding if a full binding is unattainable. In our example, it would mean that we get the value [10, 15] because a full binding is possible in this case.

bar现在有值[10,15]:int list或者bar是类型(int * int) - >((int * int)list)的函数,它最终将返回一个头部为元组的列表(10,15)?为了实现这个目的,我在对namin的答案的评论中提出,在类型系统中需要一个额外的规则,即实际参数与形式参数的绑定是“最可能的”,即系统应该尝试非部分绑定首先,如果完全绑定无法实现,则只尝试部分绑定。在我们的例子中,这意味着我们得到值[10,15],因为在这种情况下可以完全绑定。

Is such a concept of "fullest possible" inherently meaningless? I don't know, but I can't immediately see a reason it would be.

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

One problem is of course if you want the second interpretation of the last code snippet, then you'd need to go jump through an extra hoop (typically an anonymous function) to get that.

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

#5


Tuples are not present in these languages simply to be used as function parameters. They are a really convenient way to represent structured data, e.g., a 2D point (int * int), a list element ('a * 'a list), or a tree node ('a * 'a tree * 'a tree). Remember also that structures are just syntactic sugar for tuples. I.e.,

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

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

is a convenient way of handling a (string * int * string) tuple.

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

There's no fundamental reason you need tuples in a programming language (you can build up tuples in the lambda calculus, just as you can booleans and integers—indeed using currying*), but they are convenient and useful.

在编程语言中你需要元组没有根本原因(你可以在lambda演算中建立元组,就像你可以使用布尔和整数一样 - 确实使用currying *),但它们既方便又有用。

* E.g.,

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