【问题标题】:Typed FP: Tuple Arguments and Curriable ArgumentsTyped FP: Tuple Arguments 和 Curriable Arguments
【发布时间】:2009-01-02 00:47:53
【问题描述】:

在静态类型的函数式编程语言中,例如标准 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)”,这两种变体都可以同样容易地被柯里化。

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

【问题讨论】:

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


    【解决方案1】:

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

    为什么会这样?标准 ML 是这些语言中最古老的一种,当人们第一次编写 ML 编译器时,还不知道如何有效地处理柯里化参数。 (问题的根源在于 any curried 函数 could 被您尚未见过的其他代码部分应用,您必须使用记住这种可能性。)自从设计了标准 ML 以来,编译器得到了改进,您可以在 excellent paper by Simon Marlow and Simon Peyton Jones 中了解最新技术。

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

    【讨论】:

    • 这里有两种更喜欢元组形式的函数式语言:Nemerle 和 Scala。两者都需要与 OO 语言(如 F# 一样)集成,并希望它们的库分别可从 CLR/JVM 使用。两者都允许使用 _ + 3 等语法进行部分应用。
    • 有些 Lisps 也可以进行部分应用。 Clojure 有 #(+ (% 3)),Scheme 有 SRFI 26:(cut + <> 3)
    • 感谢 Norman 提供的背景资料,以及我会感兴趣地阅读的论文。感谢 Alexey 提供其他语言的信息。
    【解决方案2】:

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

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

    【讨论】:

    • 谢谢,好点子。我认为答案一定是否定的,而且,是的,我可以看到 a->(aa) 与 (aa)->a 的组合如何工作的问题,即如果后者是一个多态函数,应该认为它是完全应用的还是只是柯里化的?
    • 尽管可以通过向类型系统添加一条规则来解决这个问题,即应该使用“尽可能完整”的绑定。
    • 但是如何根据部分应用解决“尽可能完整”的绑定?
    • 我会用一些代码示例做一个单独的答案,如果你愿意,你可以指出我推理中的任何明显错误:-)
    【解决方案3】:

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

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

    【讨论】:

    • 谢谢,curryuncurry 很有趣,我想他们表明这在原则上是可行的。我问的部分原因是文体,部分原因是柯里化是这些语言的主要广告优势之一。这种区分对我来说似乎很不自然。
    【解决方案4】:

    在 namin 的好答案下扩展 cmets:

    假设一个函数的类型为'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
    

    但是你可以设想一个多态函数来代替最后一个代码 sn-p 中的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],因为在这种情况下可以进行完全绑定。

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

    一个问题当然是如果您想要最后一个代码 sn-p 的第二种解释,那么您需要跳过一个额外的圈(通常是一个匿名函数)来获得它.

    【讨论】:

      【解决方案5】:

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

      【讨论】:

      • 谢谢。我知道这个。我的兴趣更多是在另一个方向上,不是取消元组,而是使用元组参数并且仍然能够部分应用函数。
      猜你喜欢
      • 2013-08-26
      • 2018-07-09
      • 1970-01-01
      • 2015-08-10
      • 1970-01-01
      • 2011-10-31
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多