类型化 FP:元组参数和可柯里化参数

发布于 2024-07-11 15:31:13 字数 584 浏览 8 评论 0原文

在静态类型函数编程语言中,例如 Standard 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)”,允许同样轻松地柯里化两个变体。

我认为当设计我提到的四种语言时,一定会出现这个问题。 那么有人知道为什么这四种语言选择不“统一”这两种类型模式的任何原因或研究吗?

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:

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.

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.

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.

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?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

假情假意假温柔 2024-07-18 15:31:13

我认为今天的共识是通过柯里化(a -> b -> c 形式)来处理多个参数,并在您真正想要以下值时保留元组元组类型(在列表等中)。 自标准 ML 以来的所有静态类型函数语言都遵守这一共识,标准 ML(纯粹作为惯例)对采用多个参数的函数使用元组。

为什么会这样呢? 标准机器学习是这些语言中最古老的,当人们第一次编写机器学习编译器时,并不知道如何有效地处理柯里化参数。 (问题的根源在于,任何柯里化函数都可能被您尚未见过的其他代码部分应用,并且您必须使用以下命令来编译它考虑到这种可能性。)自从设计了标准机器学习以来,编译器已经得到了改进,您可以在 西蒙·马洛和西蒙·佩顿·琼斯的优秀论文

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

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.

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.

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

烛影斜 2024-07-18 15:31:13

不将柯里化和非柯里化函数类型混为一谈的至少一个原因是,当元组用作返回值(这些类型化语言中返回多个值的一种便捷方式)时,会非常混乱。 在合并类型系统中,函数如何保持良好的可组合性? 会 a -> (a * a) 也转换为 a -> 一个-> 一个? 如果是这样,则为 (a * a) -> aa -> (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 又如何呢? (应用 vecSum (1,1))[(0,1)]? 事情变得复杂了! 您最完整的解包是否意味着 (1,1) 是用 apply 的 a & 解包的? b 参数? 这不是意图……无论如何,推理变得复杂。

简而言之,我认为很难将柯里化函数和非柯里化函数混为一谈,同时(1)保留旧系统下有效的代码语义以及(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?

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:

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.

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.

风透绣罗衣 2024-07-18 15:31:13

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

至少在 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相同。 我想类似的事情在其他语言中也是可能的。 自动柯里化元组上定义的每个函数会有什么更大的好处? (因为这似乎就是你想问的。)

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:

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.)

溺深海 2024-07-18 15:31:13

扩展 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) -> ('列表)。 现在的构图会有问题。 考虑:

let bar = gimme_list(gimme_tuple(5))

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

这种“尽可能”的概念本身就毫无意义吗? 我不知道,但我无法立即看出原因。

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

Expanding on the comments under namin's good answer:

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

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

Then assume a function which has type ('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:

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

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

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.

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.

清风无影 2024-07-18 15:31:13

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

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

是处理 (string * int * string) 元组的便捷方法。

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

* 例如,

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

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.,

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

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

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.

* 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
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文