One of the key aspects of F# (and other functional languages) is that it provides a specific syntax to model those characteristics of inputs and outputs that belong to a well predefined collection. These are called discriminated unions.
The type for our food
vendingMachine would be:
type FoodProduct = | Chips | Chocolate | Candy
while for the electronics we could have:
type Electronics = | Phones | Speakers | Headphones
🔔 It is customary to use
PascalCasefor types (i.e.,
❗️ Case types in a discriminated union should start with an uppercase letter (
In the expressions above we are defining a type with a name (
Electronics) that can have several case types. It is important to stress the fact that the cases of each discriminated union type are disjoint, that is, can not be accessed at the same time. For example:
let d = FoodProduct.Chips let s = Electronics.Speakers
The languages uses the dot
. to represent a case of a discriminated union. The value
s represents of course an speaker, and since it is inmutable, there is no possible way that can be a phone or a headphone.
So how can we define functions with discriminated unions? Let us write a function
price that receives an input of type
Electronics and gives us the price:
let price electronic = match electronic with | Phones -> 435 | Speakers -> 29 | Headphones -> 122 printfn "The price of speaker is: %A $" (price s)
The price of speaker is: 29 $
The way one can disaggregate the different cases of an input that is a discriminated union is through pattern matching, represented by the construct
match ... with and then, all the cases. The syntax is pretty straightforward: for each discriminated union case label (after the
| sign in the construct) the function returns the price as an
Note also that the match should contain all the possible cases of the discriminated union. If one is forgetting some case, the compiler will tell us about with some wiggled underlining at the match. This means that the patter matching is exhaustive.
Another very important aspect of the pattern matching is that is evaluated in the order is written.
But, what if we want to write down a
priceFood function, and asign a 1.5 price to all the items except Chocolates, that are tagged at a 2.35 price? The language introduces the wildcard symbol that matches any input in the pattern matching construct. The wildcard is represented by the
_ (single underscore) symbol:
let priceFood food = match food with | Chocolate -> 2.35 | _ -> 1.5 printfn "Chocolate: %A $" (priceFood FoodProduct.Chocolate) printfn "Chips: %A $" (priceFood FoodProduct.Chips) printfn "Candy: %A $" (priceFood FoodProduct.Candy)
Chocolate: 2.35 $ Chips: 1.5 $ Candy: 1.5 $
Here we see the interplay between the order evaluation of the pattern matching and the wildcard. We are returning a specific values for some cases (Chocolate), and asign a common value for the rest of the cases. When a food
value is received by
priceFood it is compared with the
Chocolate case first. If it is not chocolate, then is get captured by the wildcard pattern. Consider the following example:
let priceSale food = match food with | _ -> 1.5 | Chocolate -> 2.35 printfn "Chocolate: %A $" (priceSale FoodProduct.Chocolate) printfn "Chips: %A $" (priceSale FoodProduct.Chips) printfn "Candy: %A $" (priceSale FoodProduct.Candy)
Chocolate: 1.5 $ Chips: 1.5 $ Candy: 1.5 $
Well, everything is “on sale” at 1.5! That is because the wildcard captures any input and since it is the first case, any food will be at that price.
Here again, the compiler behind scenes comes to our rescue. It will let you know that some of the pattern matching rules will not be reached:
In many cases, the discriminated union construct is not general enough, so one can combine it with basic types. Let us say that we want to identify the brand of each of our items in the vending machines. Since it can accomodate multiple brands of products, we decide that we represent the brand by a
string. We can expand our
FoodProduct discriminated union type as:
type BrandedFood = | Chips of string | Chocolate of string | Candy of string
Each of the cases of the
BrandedFood type has now a value of type
string. The discriminated union makes use of the keyword
of to associate each case with each value type. One can define identifiers for this compound type as:
let belovedChocolate = BrandedFood.Chocolate "Wonka" let healthyChips = BrandedFood.Chips "NotALeis" let sourCandy = BrandedFood.Candy "TearDrops"
In the brand example, we choose to combine all the cases with the same basic type,
string. But, again, one can mix and match. For example, a chocolate can come in different presentations:
type ChocolatePresentation = | Bar of float // a chocolate Bar of a given weight | Box of int // a package with a number of chocolate pieces
Or, if we want to model the change money the vending machine returns to the customer, we can define a case where an actual amount is returned, and another one that represents the fact that the client just put the exact money into the machine:
type Change = | Amount of float | NoChange
Pattern matching in functions against these compound discriminated unions is again fairly simple. To get the brand of a
BrandedFood product, we can define the function
let brand product = match product with | Chips p -> p | Chocolate p -> p | Candy p -> p
printfn "Brand of belovedChocolate: %s" (brand belovedChocolate)
Brand of belovedChocolate: Wonka
In this function, each case has an associated value represented by the identifier
p, that is unwrapped from the discriminated union, and returned.
let changeValue change = match change with | Amount money -> money | NoChange -> 0 let c = Amount 3 printfn "You are receiving %A $ as change" (changeValue c)
You are receiving 3.0 $ as change
Note on how a discriminated union that mix basic types seems to present a kind of divergence in code. The type
Changecombines a wrapped
Amount) and a pure union type
NoChange. Since functions need to have specific input and output types, any function that receives a
Changeinput has to return a defined output. There usually two possibilities. The first one is that the function flattens the inputs (such as in the case of
changeValue), where one gets the float value of the money the user receives. The second one corresponds to a function that promotes the input types to another one: for example, the trivial case would be a function that prints the amount of money the customer receives,
printChange: Change -> (), that is, receives a
unit. This is one of the key ideas behind functional programming, being able to connect different types through functions.
The last, but not least, use case of discriminated unions correspond to those ones that only have one term. Let us say that we need to describe the models of the different items in our electronics vending machine. One way to do it is using a single discriminated union:
type Model = | Model of string
Then, we can define different models of items:
let yPhone = Model "Xtreme 3S" let miniSpeakers = Model "Louder Pro"
Since the discriminated union has only one case, there is a short way to unwrap the value inside it:
let (Model yPhoneModel) = yPhone printfn "The model of the yPhone is %A" yPhoneModel
The model of the yPhone is "Xtreme 3S"
Looks like there is a lot of pomp in this types, why just not use a simpler
string instead? The answer is, again, related with functions. Let us define the
printModel function as:
let printModel (model: Model) = let (Model value) = model printfn "The model is %A" value
printModel yPhone printModel miniSpeakers
The model is "Xtreme 3S" The model is "Louder Pro"
The signature of the
printModel function is
Model -> (), meaning that receives a Model value and returns unit. Since the input is a
Model, there is no possible way that we can pass a plain string to it. In this way, one protects the input of the function such that it has to be the precise type we decided, and nothing else.
It should be pointed out that this is not a validation, that is, one still can mix up the meaning of
Model by building it with any string, the single piece of code that is the function
printModel is guarded by the single discriminated union type. Then, this type does not replace validation, but gives you a cleaner code.
The discriminated union type in both variants (single and multiple) is a key aspect of the functional programming in F#. The ability to describe well predefined collections with a single type simplifies codes and makes them robust. The exhaustive pattern matching specification (monitored by the F# compiler behind scenes) makes sure that the programmer takes care of all cases of the discriminated union, and not a single case is left behind, with possible holes in the code.
The discriminated union type is the way F# represents a sum type, as this kind of types are called in algebraic type theory.