In this post, we'll look at the control flow expressions, namely:
These control flow expressions are no doubt very familiar to you. But they are very "imperative" rather than functional.
So I would strongly recommend that you do not use them if at all possible, especially when you are learning to think functionally. If you do use them as a crutch, you will find it much harder to break away from imperative thinking.
To help you do this, I will start each section with examples of how to avoid using them by using more idiomatic constructs instead. If you do need to use them, there are some "gotchas" that you need to be aware of.
The best way to avoid
if-then-else is to use "match" instead. You can match on a boolean, which is similar to the classic then/else branches. But much, much better, is to avoid the equality test and actually match on the thing itself, as shown in the last implementation below.
// bad let f x = if x = 1 then "a" else "b" // not much better let f x = match x=1 with | true -> "a" | false -> "b" // best let f x = match x with | 1 -> "a" | _ -> "b"
Part of the reason why direct matching is better is that the equality test throws away useful information that you often need to retrieve again.
This is demonstrated by the next scenario, where we want to get the first element of a list in order to print it. Obviously, we must be careful not to attempt this for an empty list.
The first implementation does a test for empty and then a second operation to get the first element. A much better approach is to match and extract the element in one single step, as shown in the second implementation.
// bad let f list = if List.isEmpty list then printfn "is empty" else printfn "first element is %s" (List.head list) // much better let f list = match list with |  -> printfn "is empty" | x::_ -> printfn "first element is %s" x
The second implementation is not only easier to understand, it is more efficient.
If the boolean test is complicated, it can still be done with match by using extra "
when" clauses (called "guards"). Compare the first and second implementations below to see the difference.
// bad let f list = if List.isEmpty list then printfn "is empty" elif (List.head list) > 0 then printfn "first element is > 0" else printfn "first element is <= 0" // much better let f list = match list with |  -> printfn "is empty" | x::_ when x > 0 -> printfn "first element is > 0" | x::_ -> printfn "first element is <= 0"
Again, the second implementation is easier to understand and also more efficient.
The moral of the tale is: if you find yourself using if-then-else or matching on booleans, consider refactoring your code.
If you do need to use if-then-else, be aware that even though the syntax looks familiar, there is a catch that you must be aware of: "
if-then-else" is an expression, not a statement, and as with every expression in F#, it must return a value of a particular type.
Here are two examples where the return type is a string.
let v = if true then "a" else "b" // value : string let f x = if x then "a" else "b" // function : bool->string
But as a consequence, both branches must return the same type! If this is not true, then the expression as a whole cannot return a consistent type and the compiler will complain.
Here is an example of different types in each branch:
let v = if true then "a" else 2 // error FS0001: This expression was expected to have // type string but here has type int
The "else" clause is optional, but if it is absent, the "else" clause is assumed to return unit, which means that the "then" clause must also return unit. You will get a complaint from the compiler if you make this mistake.
let v = if true then "a" // error FS0001: This expression was expected to have type unit // but here has type string
If the "then" clause returns unit, then the compiler will be happy.
let v2 = if true then printfn "a" // OK as printfn returns unit
Note that there is no way to return early in a branch. The return value is the entire expression. In other words, the if-then-else expression is more closely related to the C# ternary if operator (
One of the places where if-then-else can be genuinely useful is to create simple one-liners for passing into other functions.
let posNeg x = if x > 0 then "+" elif x < 0 then "-" else "0" [-5..5] |> List.map posNeg
Don't forget that an if-then-else expression can return any value, including function values. For example:
let greetings = if (System.DateTime.Now.Hour < 12) then (fun name -> "good morning, " + name) else (fun name -> "good day, " + name) //test greetings "Alice"
Of course, both functions must have the same type, meaning that they must have the same function signature.
The best way to avoid loops is to use the built in list and sequence functions instead. Almost anything you want to do can be done without using explicit loops. And often, as a side benefit, you can avoid mutable values as well. Here are some examples to start with, and for more details please read the upcoming series devoted to list and sequence operations.
Example: Printing something 10 times:
// bad for i = 1 to 10 do printf "%i" i // much better [1..10] |> List.iter (printf "%i")
Example: Summing a list:
// bad let sum list = let mutable total = 0 // uh-oh -- mutable value for e in list do total <- total + e // update the mutable value total // return the total // much better let sum list = List.reduce (+) list //test sum [1..10]
Example: Generating and printing a sequence of random numbers:
// bad let printRandomNumbersUntilMatched matchValue maxValue = let mutable continueLooping = true // another mutable value let randomNumberGenerator = new System.Random() while continueLooping do // Generate a random number between 1 and maxValue. let rand = randomNumberGenerator.Next(maxValue) printf "%d " rand if rand = matchValue then printfn "\nFound a %d!" matchValue continueLooping <- false // much better let printRandomNumbersUntilMatched matchValue maxValue = let randomNumberGenerator = new System.Random() let sequenceGenerator _ = randomNumberGenerator.Next(maxValue) let isNotMatch = (<>) matchValue //create and process the sequence of rands Seq.initInfinite sequenceGenerator |> Seq.takeWhile isNotMatch |> Seq.iter (printf "%d ") // done printfn "\nFound a %d!" matchValue //test printRandomNumbersUntilMatched 10 20
As with if-then-else, there is a moral; if you find yourself using loops and mutables, please consider refactoring your code to avoid them.
If you want to use loops, then there are three types of loop expressions to choose from, which are similar to those in C#.
for-in-do. This has the form
for x in enumerable do something. It is the same as the
foreachloop in C#, and is the form most commonly seen in F#.
for-to-do. This has the form
for x = start to finish do something. It is the same as the standard
for (i=start; i<end; i++)loops in C#.
while-do. This has the form
while test do something. It is the same as the
whileloop in C#. Note that there is no
do-whileequivalent in F#.
I won't go into any more detail than this, as the usage is straightforward. If you have trouble, check the MSDN documentation.
As with if-then-else expressions, the loop expressions look familiar, but there are some catches again.
Here's an example of the unit constraint. The expression in the loop should be unit, not int, so the compiler will complain.
let f = for i in [1..10] do i + i // warning: This expression should have type 'unit' // version 2 let f = for i in [1..10] do i + i |> ignore // fixed
One of the places where loops are used in practice is as list and sequence generators.
let myList = [for x in 0..100 do if x*x < 100 then yield x ]
I'll repeat what I said at the top of the post: do avoid using imperative control flow when you are learning to think functionally. And understand the exceptions that prove the rule; the one-liners whose use is acceptable.