The Road to Functional Programming in F# – From Imperative to Computation Expressions

In F# there are a lot of options when it comes to choosing the style in which you will perform a computation.  So, for our last meeting of the the NYC F# User Group I decided to try and build a general understanding of how the different styles are related to each other through trying them and discussing our results. Come along with us and explore five of different styles of programming in F#.

For our example I chose Project Euler #1 as it’s simple and wouldn’t get in the way of contrasting the different computation styles.

Find the sum of all the multiples of 3 or 5 below 1000.

As there are a lot of people out there that are new to F#, let’s start with an imperative approach.  That way we can all feel comfortable and see how the other styles relate.

let pe1_while limit =
    let mutable acc = 0
    let mutable x = 0
    while x < limit do
        if x % 5 = 0 || x % 3 = 0 then acc <- acc + x
        x <- x + 1
    acc

pe1_while 1000

Next, let’s move on to doing the same thing recursively.  This transition is the first step on the long road of functional programming and it’s important that new users see how the two basic patterns map to each other.  For this reason, I kept everything but the style of computation the same.

let pe1_rec limit =
    let rec inner_pe1 x acc =
        if x < limit then
            if x % 5 = 0 || x % 3 = 0 then
                inner_pe1 (x + 1) (x + acc)
            else
                inner_pe1 (x + 1) acc
        else acc
    inner_pe1 0 0

As you can see, the combination of the first if and the recursive calls correspond to the while statement.  The predicate remains the same.  The biggest conceptual hurdle for new users here is that for both predicates a course of action must be explicitly specified in both the success and failure case.

Another important idea here is that when doing recursion you don’t want to leave anything left undone on the stack. If you do, the compiler can’t do tail call optimization. Without tail call optimization each recursive call will create an additional stack frame. This slows things down and can even crash your program by overflowing the stack if the recursion goes too deep. As long as you don’t leave anything left undone your recursive function will run just as fast as a while loop.

I constructed this example with the thought that it would be best to avoid having to explain too many new constructs at once.  However, while discussing this solution one new user remarked that this looked like spaghetti code.  In response to this Paul, one of our more advanced users, whipped up a much nicer solution utilizing pattern matching.

let pe1_rec2 limit =
    let rec inner_pe1 acc = function
        | 1                     -> acc
        | i when (i % 3 = 0)
              || (i % 5 = 0)    -> inner_pe1 (acc + i) (i - 1)
        | i                     -> inner_pe1 acc       (i - 1)
    inner_pe1 0 (limit - 1) 

Now that’s some pretty code!  While it may have some intermediate concepts, the recursion looks just beautiful when structured with a pattern match.

Still, to most I think this hardly seems worth giving up mutation for.  This is why we should move on to pipelines and comprehensions next.

let pe1_seq limit =
    seq { 0 .. limit - 1 }
    |> Seq.filter (fun x -> x % 5 = 0 || x % 3 = 0)
    |> Seq.sum

While bound to perform somewhat slower due to the use of sequences, it’s hard to imagine a more conceptually elegant solution to this problem. In comparison to the imperative version, the sequence comprehension takes on the role of both the while loop and the incrementor in generating the sequence values, the filter acts as the predicate and the sum as the accumulator.

The final two examples I’ll show you were more for fun and to provide a challenge to the intermediate and experienced users.  Conquering folding and unfolding were some of the biggest hurdles I faced when learning functional programming.  Just getting comfortable with how accumulators work takes some time for everyone.  For practice let’s use fold in our pipeline instead of filter and sum.

let pe1_fold limit =
    Seq.init limit id
    |> Seq.fold (fun acc x -> if x % 5 = 0 || x % 3 = 0
                              then acc + x
                              else acc) 0

Here we took a somewhat different route when generating sequence values by using the Seq.init function.  As I discussed in my Ted Neward’s Folding Challenge post, the contents of the folding function can be directly mapped to the imperative and recursive solutions.  Just as in the first two examples the adding is done explicitly right along with the application of the predicate.   The biggest conceptual difference in this example is that the accumulator is passed as the return value of each call to fold instead of being kept in a mutable variable as in the imperative version or explicitly passed forward as in the recursive.

Finally, as a challenge to the most experienced users let’s see if we can do this with a computation expression.

type Euler1Builder() =
    member b.Combine(x, y) = x + y
    member b.Zero() = 0
    member b.Yield(x) = if x % 5 = 0 || x % 3 = 0 then x else 0
    member b.For(vals, f) =
        vals |> Seq.fold (fun s n -> b.Combine(s, f n)) (b.Zero()) 

let eb = new Euler1Builder()

let pe1_eb limit = eb { for x = 0 to limit - 1 do yield x }

This first attempt is what I showed off at our user group meeting.  It’s embarrassingly ugly and ends up just being a convoluted way to generate a sequence and fold over the values.  Since the meeting I’ve fiddled with it a bit and have come up with a solution I like much more.

type Euler1Builder2() =
    member b.Yield(x) = if x % 5 = 0 || x % 3 = 0 then x else 0
    member b.For(generated, f) = generated |> Seq.map (fun x -> f x)
    member b.Run(filtered: int seq) = filtered |> Seq.sum 

let eb2 = new Euler1Builder2()

let pe1_eb2 limit = eb2 { for x = 0 to limit - 1 do yield x }

Instead of being like the fold example, this is much more like pipeline.  The only downside here is that the computation expression attempts to be as generic as possible and so we need a type annotation to make the example compile.  Overall, I think it ended up looking quite nice.  It’s also not nearly as complex as you might think.

Let me explain how each of the members of the computation expression fit together to make this work.  The builder’s For method allows us to use a for loop within our expression.  The values generated by that for loop are provided to the For method in the form of a sequence.  When For is called, it is passed that sequence along with a function which does whatever is scoped inside of our expression’s for loop.  In this case, that function is the builder’s Yield method because we are yielding each value.  Finally, the entire computation is preformed within the context of the Run method, which allows the input and output of the expression to be manipulated.  So, within our example we generate our values with For, filter them in Yield and then finally sum them in Run.

I hope you’ve enjoyed our journey through the different styles of computation in F#. The only style I think I didn’t cover here is continuation passing, which is more of a hack to get around the limits of recursion and linked lists anyway.  If you do think of something I missed, want to discuss something related or just feel like being friendly I hope you’ll leave a comment.

Leave a Reply

Blog at WordPress.com.

Discover more from Inviting Epiphany

Subscribe now to keep reading and get access to the full archive.

Continue reading