Reputation: 788
In F#, is it possible for a function to take one required parameter and one or more optional parametes depending on context? In the following toy example, whisk initially takes eggYolks as its only parameter but, in the very next step, it takes the output of the initial step plus granulatedSugar and marsalaWine. Is this possible and how do I feed the additional ingredients to tiramisu and print out both steps to the console?
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)
Upvotes: 3
Views: 722
Reputation: 36688
Summary: You should write whisk
to take a list. See below for the full explanation, which starts with the wrong approach, explains why it's the wrong approach, and then moves to the right approach.
Long explanation:
The question you're asking is whether you could write the function whisk
to take multiple things to be whisked, e.g. you're asking whether a whisk
function could look like:
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
But this design has some problems. For one thing, this function's type signature is inconvenient: the first parameter is an ingredient, but the second and third parameters might be ingredients (they're actually Option
s). In other words, if you had specified the types of the parameters in your function, they would have looked like:
type Ingredient = string // For this example
let whisk (item1 : Ingredient) (maybeItem2 : Ingredient option) (maybeItem3 : Ingredient option) =
// ... function body goes here ...
Why is this inconvenient? Well, if you only wanted to whisk a single thing, you'd have to call this function as whisk eggYolks None None
. (Calling it without the two None
parameters would get you a partially-applied function, which is a different topic). And another inconvenience: this is limited to just three items; if you wanted to whisk four items, you'd have to change the function signature, and then you'd have to change everywhere it was called to pass four parameters by adding an extra None
to each call.
Also, this example function doesn't actually return anything, for simplicity. If it did return something, it gets even more complicated. For example, if you're coming from an imperative language like C#, you might try writing this:
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
But that's ugly. When your F# code starts looking ugly, that's usually a sign that your design is off, somehow. For example, maybe you could let the whisk
function take a list of ingredients, instead of trying to pass multiple parameters where some of them might be None
. E.g., the whisk
function would instead look like:
let whisk (items : Ingredient list) =
// ... function body goes here ...
And then you'd call it like this:
let whiskedEggYolks = whisk [eggYolks]
let mixture = whisk [whiskedEggYolks; granulatedSugar; marsalaWine]
What would that function look like inside? Well, it would probably apply some transformation to each ingredient, then some combining function to combine all those ingredients together into a single result. In F#, "apply some transformation to each item" is called map
, and "apply some combining function to combine multiple items into a single one" is either fold
or reduce
. (I'll explain the difference between fold
and reduce
below). Here, I think you'd want reduce
, since whisking an empty bowl doesn't make sense. So our whisk
function becomes:
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)
When you whisk "70<g> of egg yolks"
, you get "70<g> of egg yolks, whisked"
. Then when you whisk that together with "100<g> of granulated sugar"
and "120<ml> of sweet marsala wine"
, you get the output:
"70<g> of egg yolks, whisked, plus 100<g> of granulated sugar, whisked, plus 120<ml> of sweet marsala wine, whisked"
And yet your function is beautifully simple (just three lines to handle any number of ingredients!) and you didn't have to write any of the list-handling code, since that was taken care of by the standard F# core library functions List.map
and List.reduce
. That sort of elegance is what you should be aiming for when you do functional programming.
I said I'd explain the difference between fold
and reduce
. The main difference is whether you expect to be dealing with empty collections sometimes. The reduce
function requires that there be at least one item in the collection you're reducing, and doesn't need an initial value since the first item of the collection is taken as the initial value. But because reduce
needs the first item of the collection to set its initial value, it will throw an exception if it is passed an empty collection, because there's no way for it to know what value to use. (F# deliberately avoids null
, for good reason -- so it's not always possible to determine a good value for an empty collection). Whereas fold
requires you to specify an explicit initial value, but it is okay with an empty collection, because if you pass an empty collection then it just returns the default value. E.g.,
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
See also Difference between fold and reduce? for more explanations.
Upvotes: 2