Fork me on GitHub

Argue for robustness

So many of us working on a daily basis with F# always claim that we are able to make more robust and bulletproof applications with fewer lines of code than we would need if we used the C’s (C,C++,C#, …). So how do we achieve this?

I will try to explain this in a less theoretical way so people don’t get lost in translation. Besides I will provide the usual foo/bar examples as well as a basic real world example.

Let’s start by defining a couple of functions:

let log a b = System.Console.WriteLine(sprintf "-Log: %A (%A)" a b)

let foo x    = try 2*x |> Some with ex -> log x ex; None
let bar x    = try 2+x |> Some with ex -> log x ex; None
let foobar x = try 2/x |> Some with ex -> log x ex; None
val log : a:'a -> b:'b -> unit
val foo : x:int -> int option
val bar : x:int -> int option
val foobar : x:int -> int option

We can all agree that the function look pretty robust right? The main operation is performed inside a try/with statement, for the C’s think of it as a try/catch statement. Now if the operation fails, 2/0 is possible in foobar, the log function will be called with the input parameter x and the exception ex. What seems a bit strange in the functions is that both operations, try/with, finishes in Some/None. This is one of the powerful features of F#, Some/None is a union type between the type and no-value. In other words, either you have a value of the given type Some of 'a or you don’t any value at all None. If you are familiar to ML-like languages, you will have seen this as datatype 'a option = NONE | SOME of 'a, in a identical form for OCaml as type 'a option = None | Some of 'a (you might be able to argue that F# is the OCaml .NET version) and finally as data Maybe a = Just a | Nothing in Haskell.

Remark: Just for correctness, the log function is implemented with the Console.WriteLine method, which is threadsafe and in combination with sprintf/"%A", to make it generic.

Robustness but verbosity

Now that we have the robust functions. lets combine a couple of them together as we do when we write code:

2 |> foo |> bar |> foobar
error FS0001: Type mismatch. Expecting a
    int option -> 'a
but given a
    int -> int option
The type 'int option' does not match the type 'int'

We can see that we get a type error as the function bar takes an int as input and not an int option type. Let’s re-write the code in a correct way:

2
|> foo
|> function | Some v -> bar v    | None -> None
|> function | Some v -> foobar v | None -> None
val it : int option = Some 0

I think it’s easy to argument for robustness and correctness but you might think: “Less code you say?”. And you are right, this kind of implementation would be really annoying to write for every single function you would have to pipe the result to.

Monads to the rescue

The more theoretical approach to simplify the code but still maintaining correctness, would be to implement the Maybe Monad (monads are called Computation expressions in F#):

type MaybeMonad() =
  member t.Bind(m,f) =
    match m with
    | Some v -> f v
    | None -> None
  member t.Return v = Some v
let maybe = MaybeMonad()
type MaybeMonad =
  class
    new : unit -> MaybeMonad
    member Bind : m:'b option * f:('b -> 'c option) -> 'c option
    member Return : v:'a -> 'a option
  end
val maybe : MaybeMonad

Where we can use the monad to write the previous code as:

maybe{ let! x = foo 2
       let! y = bar x
       let! z = foobar y
       return z }
val it : int option = Some 0

By using the monad we don’t have to write function | Some v -> some_function v | None -> None for each time we pipe the value but, it’s still some kind of annoying having to write all the temporary variables x,y,z in order to get the final result. The ideal scenario would be to write the following code:

maybe{ return 2 |> foo |> bar |> foobar }
error FS0001: Type mismatch. Expecting a
    int option -> 'a
but given a
    int -> int option
The type 'int option' does not match the type 'int'

But this is not possible as we need to bind the functions together. Actually that is what let! does. The let! operator is just syntactic sugar for calling the Bind method.

Remark: The Maybe Monad can be implemented in less verbose code by using the built-in Option.bind function:

type MaybeMonad() =
  member t.Bind(m,f) = Option.bind f m
  member t.Return v = Some v
let maybe = MaybeMonad()

Infix operator to the rescue (»=)

So how do we get as close to 2 |> foo |> bar |> foobar but without compromising on correctness and robustness? Well the answer is quite simple

What we need to do is to introduce the following infix operator:

let (>>=) m f = Option.bind f m
val ( >>= ) : m:'a option -> f:('a -> 'b option) -> 'b option

Now we can combine functions together in the following manner:

2 |> Some >>= foo >>= bar >>= foobar
> val it : int option = Some 0

Which is pretty close to what we wanted to achieve, 2 |> foo |> bar |> foobar, right?

Another thing to have in mind when using binded functions is to think of the bind as how Short-circuit evaluation works. SCE denotes the semantics of some Boolean operators in some programming languages in which the second argument is executed or evaluated only if the first argument does not suffice to determine the value of the expression. For example: when the first argument of the AND function evaluates to false , the overall value must be false; and when the first argument of the OR function evaluates to true, the overall value must be true. Binding functions is more or less the same, where the output from the first function is bounded to the input of the second. If the first function returns None, then the second is never called and None is returned for the whole expression. Let’s see this in an example using foobar and 0 as input:

0 |> Some >>= foobar >>= foobar >>= foobar 
> -Log: 0 (System.DivideByZeroException: Division by zero 
  at FSI_0045.foobar (Int32 x) [0x00000] in <filename unknown>:0 )
val it : int option = None

After foobar throws an exception and return None, none of the other following foobar functions are evaluated. Cool right?

Another infix operator to the rescue (|=)

As in real life you might want to get the value of the type and use it in other frameworks that doesn’t have support for Some/None . What you can do is to do something like:

42 |> Some |> function | Some v -> printfn "%A" v | None -> ()
> 42
val it : unit = ()

or

42 |> Some |> function | Some v -> v | None -> failwith "Some error"
val it : int = 42

This will limit your code to unit = () or to throw and exception. which would be OK if it’s encapsulated in a try/with statement. But sometimes you will just want be able to assign a value that means no change in the final result of the computation. For example: 0 in a sum of integers, 1 in a product of integers, an empty list in a concatenation, and so on. To achieve this I usually implement the following infix operator:

let (|=) a b = match a with | Some v -> v | None -> b
val ( |= ) : a:'a option -> b:'a -> 'a

This will now allow us to use the value as the given type and if there is no value then use the specified default value:

42 |> Some >>= foo >>= bar >>= foobar |= 0
val it : int = 0

Remark: As with the Maybe Monad, this infix operator can also be implemented in less verbose code by using the built-in Option.fold function:

let (|=) a b = a |> Option.fold(fun _ x -> x) b

So let’s use the infix operators on a basic real world example

Now that we have the receipt to create correct and robust one-liner functions, let’s define two functions for this example. The first will return Some array of even numbers from an arbitrary array. And the second will return Some array of the top 10 biggest numbers from an arbitrary array.

let even a =
  try  a |> Array.filter(fun x -> x % 2 = 0) |> Some
  with ex -> log a ex; None

let top10 a =
  try  Array.sub (a |> Array.sortBy(~-)) 0 10 |> Some
  with ex -> log a ex; None
val even : a:int [] -> int [] option
val top10 : a:int [] -> int [] option

For the first function it’s easy to argument for it to never break. If the array doesn’t contain any even numbers, Some empty array will be returned. But for the second function we can see that there will always be returned a Some sub-array of size 10. What will happen when the input array is of a smaller size? Let’s execute the code:

[|0 .. 2000|] |> Some >>= even >>= top10 |= Array.empty

[|0 .. 12|] |> Some >>= even >>= top10 |= Array.empty
> val it : int [] =
  [|2000; 1998; 1996; 1994; 1992; 1990; 1988; 1986; 1984; 1982|]
> -Log: [|0; 2; 4; 6; 8; 10; 12|] (System.ArgumentException: 
The index is outside the legal range.
Parameter name: count
  at Microsoft.FSharp.Collections.ArrayModule.GetSubArray[Int32]
    (System.Int32[] array, Int32 startIndex, Int32 count) [0x00000] 
      in <filename unknown>:0 
  at FSI_0015.top10 (System.Int32[] a) [0x00000] in <filename unknown>:0 )
val it : int [] = [||]

We can see that the first evaluation returns an array of ten even numbers from 2000 to 1982 while the second returns an empty array and logs the out of boundary exception to the console.

Remark: Please never write code like this, it’s always more desirable to check for the size of the array than to get an out of boundary exception. This was just to make a point of bulletproof functions and hereby applications by using F#.

Conclusion

Well now that I gave you the receipt for creating small robust and bulletproof functions, or Lego blocks as I call them, that can easily be tested for correctness and robustness, now it’s your turn to create your blocks, combine them to create bigger blocks and make robust applications. Happy coding and remember to have fun.

Where to go from here

Finally if you want to get a deeper understanding of what is happening here, please spend an of your life watching this amazing video:

comments powered by Disqus