【问题标题】:When should I write my functions in curried form?我什么时候应该以咖喱形式编写我的函数?
【发布时间】:2013-09-18 07:07:49
【问题描述】:

我最近开始学习 F#,并遇到了一些简单示例的 curried 函数,例如:

考虑一个函数,它通过将价格p 乘以售出的单位数量n 来计算销售额。

let sales (p,n) = p * (float n);;

这个函数的类型被给出为

val sales : p:float * n:int -> float

即取一对floatint 并返回float

我们可以把它写成柯里化函数

let salesHi p n = p * (float n);;

这个函数的类型被给出为

val salesHi : p:float -> n:int -> float

即接受float 并将int函数 返回到float

在简单的情况下,这似乎没有什么区别

sales (0.99, 100);;
salesHi 0.99 100;;

两个都给

val it : float = 99.0

但是,使用 curried 函数,我可以输入特定项目的价格以获得新功能。例如

let salesBeer =  salesHi 5.99;;
let salesWine =  salesHi 14.99;;

然后salesBeer 2 给出11.98salesWine 2 给出29.98

另外,我注意到像+ 这样的内置运算符被定义为函数,所以我可以这样写,例如:

let plus2 = (+) 2;
List.map plus2 [1;3;-1];;

得到

val it : int list = [3; 5; 1]

这似乎是一件好事。因此,当我想用​​命令式语言实现一个函数,该函数将采用 n > 1 参数时,我是否应该总是在 F# 中使用柯里化函数(只要参数是独立的)?或者我应该采取简单的路线并使用带有n-tuple 的常规函数​​并在必要时稍后使用curry?还是别的什么?

F# 程序员如何决定何时以柯里化形式创建函数或使用带有元组的常规函数​​?

【问题讨论】:

  • 术语 higher-order function 通常是指接受/返回函数的函数,而您的问题似乎与“元组”与“多个参数”有关。你可能想稍微改写一下。也就是说,为了柯里化,我更喜欢多参数函数。
  • @MarcinŁoś 谢谢我添加了一些关于salessalesHi 类型的内容,以使其更清晰。也有可能我没有在我的问题中使用标准术语。
  • 所谓的“高阶”函数实际上是一个curried 函数,而不是一个元组形式的函数。使用函数式语言时,函数通常采用柯里化形式,因为这允许部分应用。
  • @Lee 感谢您的澄清和编辑。在我正在阅读的文本中,像 (+)sales 这样的函数被称为高阶函数,并且柯里化甚至不在索引中(但是它出现在练习中)。我把它放在another question 中,因为我似乎使用了不恰当的术语,但考虑到书中所说的以及这里有经验的 F#ers 所说的话,我不确定什么是正确的。

标签: f# functional-programming


【解决方案1】:

当您在 curried 和 tupled 形式之间进行选择时,要考虑的主要是您作为参数的 tuple 是否意味着

元组形式。例如,float * float 可能代表一个范围,那么最好使用元组形式。

let normalizeRange (lo, hi) = if hi < lo then (hi, lo) else (lo, hi)
let expandRange by (lo, hi) = (lo - by, hi + by)

这样做的好处是您可以编写适用于范围的函数。例如,您可以编写如下内容:

randomRange() |> normalizeRange |> expandRange 10

柯里化形式。 另一方面,如果所有参数的元组不是具有某些有用含义的独立值,则柯里化形式是更好的选择。例如,幂函数 pown 2.0 10 - 两个参数是幂数和指数,但您不太可能在程序的其他地方使用元组 (2.0, 10)

当您有一个“更重要”的参数时,柯里化形式也很有用,因为您可以使用流水线。例如,List.map 必须被柯里化才能允许这样做:

[1 .. 10] |> List.map (fun n -> n + 1)

【讨论】:

  • 非常感谢,将其与您的other answer 放在一起,您已经明确解释了什么是高阶函数和柯里化函数以及何时使用它们。
【解决方案2】:

或者我应该采取简单的路线并使用带有 n 元组的常规函数​​,然后在必要时使用 curry?

你觉得做更多的工作更简单吗? let stuff x y z = ... 不仅比 let stuff (x, y, z) 输入更少,而且实际上语言所做的工作更少。第二个版本必须分配一个元组,然后将元组解构为参数,而第一个版本只使用参数。

柯里化形式是在 F# 中编写函数的惯用方式。除非数据已经存储为元组,否则我真的想不出使用元组形式的充分理由。

【讨论】:

  • 也许“更简单”这个词用错了。也许我应该说更熟悉,就像我一直在使用的命令式语言一样。在我正在阅读的教科书中,首先介绍了采用元组的函数,例如gcd : int * int -&gt; int 是最大公分母,然后是文本中所谓的高阶函数,几乎没有提示(到目前为止)哪个“更好”。你已经很好地回答了我的问题,谢谢——“惯用方式”是我所追求的。
【解决方案3】:

另一个考虑因素 - 如果您计划与 C# 或 VB .NET 进行互操作,请不要使用 curried 形式,因为它们不能很好地暴露给这些语言。另一方面,从 C#/VB .NET 的角度来看,元组形式是作为一组普通参数公开的,使用起来非常自然。

【讨论】:

    【解决方案4】:

    元组形式其实有点危险。它可能看起来类似于 ALGOL 风格的语言(=90% 的流行语言) - 但它的工作方式不同。

    考虑一下:

    foo bar(x,y)
    

    在所有允许这种语法的 ALGOL 风格的语言中,这意味着“使用 xy 作为参数调用 bar,并将结果传递给 foo” - 当 foo 没有成为一个方法(它可以是语法,如 2.* Python 中的print)。

    但是,在 Lambda 风格的语言(如 ML、Haskell 和 F#)中,这意味着“以 bar 作为参数调用 foo,然后使用元组 (x,y) 调用结果(这是一个函数)作为论据。

    如果您不熟悉 Lambda 风格的语言,这可能会让人很困惑。现在,如果您使用的是 curry 形式,则相当于:

    foo bar x y
    

    这和foo bar(x,y) 一样错误——但没有那么令人困惑!即使是不熟悉 Lambda 风格语言的程序员也可以很容易地认为bar 不是foo bar x y 中将调用的第一个函数。错误马上就清楚了。

    【讨论】:

    • 谢谢我正在解决这个问题。次要的一点,您的第一个示例以error FS0597: Successive arguments should be separated by spaces or tupled, and arguments involving function or method applications should be parenthesized 失败。您需要一个空格,如foo bar (x,y)。这在某种程度上说明了你的观点!
    • 这很有趣,谢谢,当我忘记用括号括起来 printfn 时,它肯定解释了错误!我的主要评论是 F# 编译器会帮助你。作为一个实际的例子,写let bar (x,y) = (y,x)案例1foo拿一对,写let foo (x,y) = (min x y, max x y)。尝试foo bar(1,2) 会出现“使用空格”错误,而foo bar (1,2) 也会失败。而foo (bar (1,2))foo(bar (1,2)) 都可以工作。 案例2foo接受一个函数和一对,写let foo f (x,y) = f (min x y, max x y)foo bar(1,2)foo (bar (1,2))失败;只有foo bar (1,2) 有效。
    • 实际上,我从未真正学习过 F#,但在 ML 或 Haskell 中不需要用空格分隔参数(当然,只要它们是单独的标识符)。我认为源自 ML 的 F# 也会发挥同样的作用。微软可能制定这条规则是为了防止我描述的错误。
    猜你喜欢
    • 1970-01-01
    • 2011-08-01
    • 2010-12-18
    • 2014-12-26
    • 2010-09-17
    • 2015-09-24
    • 2011-06-21
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多