【问题标题】:F# - Same Function With Different ParametersF# - 具有不同参数的相同功能
【发布时间】:2017-12-25 03:06:08
【问题描述】:

在 F# 中,函数是否可以根据上下文采用一个必需参数和一个或多个可选参数?在以下玩具示例中,whisk 最初将 eggYolks 作为其唯一参数,但在接下来的步骤中,它将初始步骤的输出加上 granulatedSugar 和 marsalaWine。这可能吗?我如何将其他成分添加到 提拉米苏 并将这两个步骤打印到控制台?

module Tiramisu = 

// http://www.lihaoyi.com/post/WhatsFunctionalProgrammingAllAbout.html

open System

// Ingredients.    
let eggYolks = "70<g> of egg yolks."
let granulatedSugar = "100<g> of granulated sugar."
let marsalaWine = "120<ml> of sweet marsala wine."

let whisk ingredient = printf "Whisk %s\t" ingredient

let tiramisu ingredients =
    ingredients
    |> whisk // eggYolks only.
    // |> whisk // plus granulatedSugar and marsalaWine.

[<EntryPoint>]
tiramisu eggYolks
// tiramisu (eggYolks granulatedSugar marsalaWine)

【问题讨论】:

  • 不确定您的实际应用程序是否与您的示例接近,但我认为在您的示例中,您可以通过使用序列、数组或成分列表来摆脱困境。
  • 但是,如果您真的需要真正的可选参数,则需要使用选项,如 msdn 示例所示:docs.microsoft.com/en-us/dotnet/fsharp/language-reference/…
  • 亚历山大,感谢您的 cmets。你能用玩具题(提拉米苏)用源码来说明你的观点吗?

标签: function parameters f# optional


【解决方案1】:

总结:您应该写whisk 来列出清单。完整的解释见下文,从错误的方法开始,解释为什么是错误的方法,然后转向正确的方法。

详细说明: 您要问的问题是您是否可以编写函数whisk 来处理多个要搅拌的东西,例如你在问whisk 函数是否看起来像:

let whisk item1 maybeItem2 maybeItem3 =
    printfn "Whisking %A" item1
    match maybeItem2 with
    | None -> ()
    | Some item -> printfn "Also whisking %A" item
    match maybeItem3 with
    | None -> ()
    | Some item -> printfn "Also whisking %A" item

但是这个设计有一些问题。一方面,这个函数的类型签名很不方便:第一个参数是一种成分,但第二个和第三个参数可能是成分(它们实际上是Options)。换句话说,如果你在你的函数中指定了参数的类型,它们应该是这样的:

type Ingredient = string  // For this example
let whisk (item1 : Ingredient) (maybeItem2 : Ingredient option) (maybeItem3 : Ingredient option) =
    // ... function body goes here ...

为什么不方便?好吧,如果你只想搅拌一个东西,你必须将此函数称为whisk eggYolks None None。 (在没有两个 None 参数的情况下调用它会得到一个 partially-applied function,这是一个不同的主题)。还有一个不便之处:这仅限于三个项目;如果您想搅拌四个项目,则必须更改函数签名,然后您必须更改调用它的所有位置以通过在每个调用中添加一个额外的 None 来传递四个参数。

此外,为简单起见,此示例函数实际上并没有返回任何内容。如果它确实返回了一些东西,它会变得更加复杂。例如,如果你来自像 C# 这样的命令式语言,你可以尝试这样写:

type Ingredient = string  // For this example
let whisk (item1 : Ingredient) (maybeItem2 : Ingredient option) (maybeItem3 : Ingredient option) =
    printfn "Whisking %A" item1
    let mutable mixtureSoFar = item1
    match maybeItem2 with
    | None -> ()
    | Some item ->
        printfn "Also whisking %A" item
        mixtureSoFar <- mixtureSoFar + item
    match maybeItem3 with
    | None -> ()
    | Some item ->
        printfn "Also whisking %A" item
        mixtureSoFar <- mixtureSoFar + item
    mixtureSoFar

但这很难看。当您的 F# 代码开始看起来很丑陋时,这通常表明您的设计已经过时了,不知何故。例如,也许您可​​以让whisk 函数获取一个成分列表,而不是尝试传递多个参数,其中一些参数可能是None。例如,whisk 函数看起来像:

let whisk (items : Ingredient list) =
    // ... function body goes here ...

然后你会这样称呼它:

let whiskedEggYolks = whisk [eggYolks]
let mixture = whisk [whiskedEggYolks; granulatedSugar; marsalaWine]

这个函数在里面会是什么样子?好吧,它可能会对每种成分应用一些转换,然后使用一些组合功能将所有这些成分组合在一起形成一个结果。在 F# 中,“对每个项目应用一些转换”称为 map,而“应用一些组合函数将多个项目组合成一个项目”是 foldreduce。 (我将在下面解释foldreduce 之间的区别)。在这里,我想你会想要reduce,因为搅拌一个空碗没有意义。所以我们的whisk函数变成了:

let whisk (ingredients : Ingredient list) =
    ingredients
    |> List.map (fun x -> sprintf "%s, whisked" x)
    |> List.reduce (fun a b -> sprintf "%s, plus %s" a b)

当你拂动"70&lt;g&gt; of egg yolks" 时,你会得到"70&lt;g&gt; of egg yolks, whisked"。然后当你将它与"100&lt;g&gt; of granulated sugar""120&lt;ml&gt; of sweet marsala wine" 一起搅拌时,你会得到输出:

"70<g> of egg yolks, whisked, plus 100<g> of granulated sugar, whisked, plus 120<ml> of sweet marsala wine, whisked"

而且您的函数非常简单(只需三行代码即可处理任何数量种成分!)而且您不必编写任何列表处理代码,因为这已得到注意由标准 F# 核心库函数 List.mapList.reduce 组成。 这种优雅是您在进行函数式编程时应该追求的目标。

折叠和缩小

我说过我会解释foldreduce 之间的区别。主要区别在于您是否希望有时处理空集合。 reduce 函数要求您要减少的集合中至少有一个项目,并且不需要初始值,因为将集合的第一项作为初始值。但是因为reduce需要集合的第一项来设置它的初始值,如果传入一个空集合它会抛出异常,因为它无法知道使用什么值。 (F# 故意 避免使用null,这是有充分理由的——因此并不总是可以为空集合确定一个好的值)。而fold 要求您指定一个明确的初始值,但是对于空集合也可以,因为如果您传递一个空集合,那么它只会返回默认值。例如,

let zeroInts = []
let oneInt   = [1]
let twoInts  = [1; 2]

let add x y = x + y

zeroInts |> List.reduce add  // Error
oneInt   |> List.reduce add  // Result: 1
twoInts  |> List.reduce add  // Result: 3
zeroInts |> List.fold add 0  // No error, result: 0
oneInt   |> List.fold add 0  // Result: 1
twoInts  |> List.fold add 0  // Result: 3

更多解释请参见Difference between fold and reduce?

【讨论】:

  • 在对你说 tl;dr 之后,我开始写自己的答案。在回答“你应该传递一份成分列表然后调用减少”之后,我实际上得到了你的答案并且......嗯,这是一个非常好的答案,也是我没有发布我的原因的一个原因。您是否可以在开头包含 TL;DR 以便从一开始就明确您的意图?
  • 我倾向于在“教学”模式下写答案,意思是从坏例子转向好例子。但是您是对的,有些人可能会看到很长的答案并跳过它;我将在顶部添加执行摘要。好主意。
  • 编译以下内容时出现错误:打开系统类型成分 = 字符串//成分。 let eggYolks = "70 个蛋黄。" let granulatedSugar = "100 砂糖。" " let marsalaWine = "120 甜马沙拉酒。"让拂(成分:成分列表)=成分|> List.map(乐趣x - > sprintf“%s,搅拌”x)| > List.reduce(乐趣ab - > sprintf“%s,加上%s”ab) [] 让混合物 = 搅拌 [eggYolks;粒状糖; marsalaWine] 我错过了什么?
  • @matekus EntryPointAttribute 注释了一个必须返回整数的入口点函数。让混合物...不是这样的功能。此外,我认为 let mix = ... 实际上并不会导致搅拌器进行评估,它只是为使用您指定的参数调用搅拌器的函数分配一个名称。使用这个: [] (newline) let main argv = (newline) let mixture = whisk [eggYolks; granulatedSugar; marsalaWine](newline) printfn "%s" mixture(newline) 0(newline) 并用实际的换行符替换 (newline)。最后一条语句(此处为 0)是 F# 中的返回值
猜你喜欢
  • 2018-03-03
  • 1970-01-01
  • 1970-01-01
  • 2020-01-30
  • 1970-01-01
  • 2019-11-29
  • 2012-06-24
  • 2015-01-28
  • 1970-01-01
相关资源
最近更新 更多