In the last post we finished off our dive into functions. In this post we will look at control flow. How do we make a branching decision? How do we loop through something until some condition is met?
I am going to try keep this post short. The reason for this is that although you will invariably need to use control flow expressions in your code, they are stylistically not very functional and there are usually more functional ways to achieve the same thing. We explore those more functional techniques in this and future posts.
If then else
Other thanmatch
(covered later), if
is probably the next most useful control flow expression we will touch on in this post. The if
expression takes a bool
and if true
proceeds with the then
body. Usually there is an else
and we will go through when that is necessary.
let b = true
if (b) then printfn "Is true" else printfn "Is false"
The above will print out Is true, and not print Is false.
Maybe we don't want to print out anything if the value is false
. We can do this:
let b = true
if (b) then printfn "Is true"
Now if you changed let b = false
, nothing would be printed.
What if we wanted to return a value based on some condition though without an else
?
let v = if(b) then 1 // <- Error: This 'if' expression is missing an 'else' branch.
At least the error message is pretty clear about what the problem is. With the print example we were returning unit
so it didn't matter if nothing was returned. Here the expression has to return a value because we are assigning that value.
let v = if(b) then 1 else 0
So depending on whether b
is true
or false
, v
will have a value of 1
or 0
respectively.
Lets take a look at something a bit more complex:
let divideBy d n = n/d
let numerator = 10
let denominator = 2
let j = if(denominator <> 0) then
printfn "Dividing by %i, not 0" denominator
let x = numerator |> divideBy denominator
printfn "The answer is %i" x
x
else
printfn "Dividing by 0"
0
Note that we don't have to assign this to a value, here
j
but it would be pretty pointless to return a value and not use it. The compiler will give you a warning at this point The result of this expression has type 'int' and is implicitly ignored. Consider using 'ignore' to discard this value explicitly, e.g. 'expr |> ignore', or 'let' to bind the result to a name, e.g. 'let result = expr'.
This is asking us to callignore()
in each branch of theif
expression.
We can have multiple lines in either branch, organized by indentation. Just like functions the last expression is what is returned as the value of the if-else
expression for each branch.
Scope
In the previous post I mentioned scope. This is a good opportunity to demonstrate scope. Check out the assigning of the denominator
value below.
let divideBy d n = n/d
let numerator = 10
let denominator = 0
if(denominator <> 0) then
printfn "Dividing by %i, not 0" denominator
let x = numerator |> divideBy denominator
printfn "The answer is %i" x
x
else
printfn "Dividing by 0"
let denominator = 1
printfn "Instead by %i, not 0" denominator
let x = numerator |> divideBy denominator
printfn "The answer is %i" x
x
printfn "Denominator is %i" denominator
The above prints out the following:
Dividing by 0
Instead by 1, not 0
The answer is 10
Denominator is 0
Notice here how we set a value for denominator
within the else
branch that shadows the outside one. Once we are back to the scope outside the if, denominator
is back to 0
, even though it was set to 1
in the else
branch. It was 1
for the scope of the else
branch of the expression as it was set in that scope.
If / elseif / else
It is (maybe) worth mentioning that you can have more than 2 branches by using elif
.
if(x = 1) then printfn "x is 1"
elif (x = 2) then printfn "x is 2"
else printfn "x is not 1 or 2"
We will briefly cover match
next and even when if / else seems a cleaner solution, once you are using elif
you almost certainly should be using match
instead.
Match
We will hopefully cover pattern matching in more detail in a later entry but no coverage of functional control flow is complete without match
.
Lets re-write the previous example using match
:
match x with
| 1 -> printfn "x is 1"
| 2 -> printfn "x is 2"
| _ -> printfn "x is not 1 or 2"
Note that _
is a catch-all, like else is.
This is a much cleaner and more functional way to do control flow. A nice benefit here is that the compiler gives you a warning if you are not matching exhaustively on all options of the matched value.
We will hopefully circle around to match
again when covering pattern matching as match
is far more powerful than demonstrated here.
for..in
If you need to loop through an entire collection and do something you could use the for pattern in enumerable-expression do body-expression
syntax. This is like foreach
in many other languages. Lets see what that looks like:
let numbers = [1..10]
for x in numbers do
printf "%i " x
Output: 1 2 3 4 5 6 7 8 9 10
See how you can easily create a range of values using start..finish
syntax. We use this to define numbers
. Then for each element of the list we print the value which is in x
.
We will hopefully cover collections in an upcoming post but for interest sake lets see how this would be done in a more functional way.
let numbers = [1..10]
numbers |> List.iter (printf "%i ")
Unsurprisingly the functional approach is to call the iter
function on the List
module. This iter
function has the signature ('T -> unit) -> 'T list -> unit
. Lets break that down:
('T -> unit)
: a function defining the action to take for each element in the list'T list
: the list to iterate throughunit
: returns unit so this function is designed to iterate through a function and do something, not return a value
There are many more functions for working with lists in the List
module and matching ones for Array
and Seq
.
for..to
While for..in
is for iterating over a collection, for..to
allows you to iterate from a start value to another. This is like a for
loop in other languages.
let ns = [|1..10..100|]
for i=0 to ((Array.length ns)/2) do
printf "%i " (Array.get ns i)
Output: 1 11 21 31 41 51
In our example we have an array that has numbers 1 to a 100 in increments of 10. We only iterate through half the list.
while
What if we want to iterate until a certain condition is true? The following code gets a random number until that number is 7
.
let random = new System.Random()
let aNumber() = random.Next(1,10)
let mutable n = 0
while (n <> 7) do
printf "%i " n
n <- aNumber()
Output: 0 9 9 1 6 5 2 2 6 6 2 6 6 1 2 3 6 8 8 1 3 2 2
We kept going through the while
loop until aNumber()
returned 7
.
Conclusion
In this post we looked at ways to represent branching logic and ways to iterate over values. Remember that much of this is a very imperative approach and as such is not used a lot in the function paradigm. We looked at some functional techniques for dealing with branching and looping and will continue this in future articles. Next up we look at Pattern Matching.