总结:您应该写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,而“应用一些组合函数将多个项目组合成一个项目”是 fold 或 reduce。 (我将在下面解释fold 和reduce 之间的区别)。在这里,我想你会想要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<g> of egg yolks" 时,你会得到"70<g> of egg yolks, whisked"。然后当你将它与"100<g> of granulated sugar" 和"120<ml> 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.map 和 List.reduce 组成。 这种优雅是您在进行函数式编程时应该追求的目标。
折叠和缩小
我说过我会解释fold 和reduce 之间的区别。主要区别在于您是否希望有时处理空集合。 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?。