输入的FP:元组参数和可引用的参数

时间:2009-01-02 00:47:54

标签: haskell f# functional-programming language-design type-systems

在静态类型的函数式编程语言中,如标准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)”,允许两种变体同样容易地进行曲解。

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

5 个答案:

答案 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) -> aa -> (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 add1add2相同,curry add2add1相同。我想其他语言也可能有类似的东西。有什么更大的收获可以自动调整元组中定义的每个函数? (因为那是你似乎要问的。)

答案 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>