Haskell 类型化FP:元组参数和可转换参数

Haskell 类型化FP:元组参数和可转换参数,haskell,f#,functional-programming,language-design,type-systems,Haskell,F#,Functional Programming,Language Design,Type Systems,在静态类型的函数式编程语言中,如标准ML、F#、OCaml和Haskell,编写函数时通常会使用彼此分开的参数,并使用空格将参数与函数名分开: let add a b = a + b 这里的类型是“int->(int->int)”,即一个函数,它接受一个int并返回一个函数,该函数依次接受int和int,最后返回一个int。因此,可以使用curry 还可以定义一个类似的函数,将元组作为参数: let add(a, b) = a + b 在本例中,类型变为“(int*int)-

在静态类型的函数式编程语言中,如标准ML、F#、OCaml和Haskell,编写函数时通常会使用彼此分开的参数,并使用空格将参数与函数名分开:

let add a b =
    a + b
这里的类型是“
int->(int->int)
”,即一个函数,它接受一个int并返回一个函数,该函数依次接受int和int,最后返回一个int。因此,可以使用curry

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

let add(a, b) =
    a + b
在本例中,类型变为“
(int*int)->int

从语言设计的角度来看,有什么原因不能简单地在类型代数中识别这两种类型模式?换句话说,这样“(a*b)->c”就减少到“a->(b->c)”,使得这两种变体都能同样容易地被使用


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

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

至少在哈斯克尔,从一种形式转换到另一种形式相当容易:

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
相同。我想其他语言也可能有类似的情况。自动套用元组上定义的每个函数会有什么更大的好处?(因为这似乎是您要问的。)

至少有一个原因不将curried和uncurried函数类型混为一谈,那就是当元组用作返回值时会非常混乱(在这些类型化语言中,返回多个值是一种方便的方式)。在合并类型系统中,函数如何保持良好的可组合性?
a->(a*a)
是否也会转换为
a->a->a
?如果是,那么
(a*a)->a
a->(a*a)
是否属于同一类型?如果没有,您将如何用
(a*a)->a
组合
(a*a)->a

你对作文问题提出了一个有趣的解决方案。然而,我觉得它并不令人满意,因为它不能很好地与部分应用程序相匹配,这确实是curried函数的一个关键便利。让我试着用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和uncarried函数。不过,这是一个有趣的问题。

元组在这些语言中不存在,只是用作函数参数。它们是表示结构化数据的一种非常方便的方式,例如,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

根据namin的好答案,对评论进行扩展:

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

然后假设一个函数的类型为
('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]
,因为在这种情况下,完全绑定是可能的

这种“尽可能充分”的概念本身是否毫无意义?我不知道,但我不能马上看出原因


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

我认为现在的共识是通过curry(curry)来处理多个参数(a->b->c表单)并在真正需要元组类型的值时(在列表等中)保留元组。自标准ML以来,每一种静态类型的函数式语言都尊重这一共识,标准ML(纯粹按照惯例)对具有多个参数的函数使用元组

为什么会这样?标准ML是o
let bar = gimme_list(gimme_tuple(5))