(* Monadic Error Handling in F# Monadic error handling is a new approach to error handling compared to the throwing and catching of exceptions. Exceptions do exist in F# since it's integrated into .Net, and must interoperate with the java-like C# language. However, the recommended way to handle errors is to use a "monad", a concept from functional programming. In F# there are two built-in monads for error handling: Option and Result The Option Type is defined as a simple discriminated union: type Option<'T> = | None | Some of 'T Consider a function that returns the largest number in a list. What should be done if the list is empty? You might be familiar with the following possibilities. 1. throw an exception 2. return a null pointer. When a function throws an exception, it fails to encapsulate error handling as part of the algorithm that it tries to encapsulate. Returning a null pointer takes part in Tony Hoare's "billion dollar mistake:" "I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years." The "monadic" approach replaces null with None: *) open System; let largest (A:int list) = if A.Length=0 then None else let mutable candidate = A[0] // candidate index for i in 1..A.Length-1 do if A[i] > candidate then candidate <- A[i] Some(candidate);; let max = largest [3;5;1;7;4;9;0;8]; // The problem is, max is not an int, but an Option (or int option). // So how do we compute over it? The possibility that it maybe None instead // of an integer forces us to confront the fact that an error may have occurred. match max with | Some x -> printfn "%d" x | None -> printfn "an error has occurred";; // But using match each time on an Option may get cumbersome and repetitive. // if we wished to apply a series of operations on an option value. open Option max |> map (fun x -> x+x) // double x |> map (fun x -> Math.Sqrt(x)) |> iter(fun x -> printfn "final result is %.3f" x) (* Note that (x |> map f) is equivalent to (map f x), but when applying a series of functions to a value, the `|>` operator gives cleaner syntax. In object syntax, it's the equivalent of writing x.f().g().h() instead of h(g(f(h))); Map applies the given function to the option value if it exists, and returns a new option enclosing the new value. If the value does not exist (None), then it returns None. For example, if max was Some(9), then `max |> map (fun x -> x+x)` will return Some(18). But if max was none, then it would return None. The iter function only differs from map in that the function to be applied doesn't return anything (returns type 'unit'). map/iter automatically checks if the value exists for you. You can also write your own version of map: *) let mymap func opt = match opt with | Some x -> Some(func x) | None -> None (* Here's another example of the option type, this time illustrating another monadic combinator, "bind" *) // The following functions check for arithmetic overflow, div by zero, etc... let safeadd(a:int, b:int) = let sum = a+b if (a>0 && b>0 && suma) then None // overflow else Some(sum);; let safemult(a:int, b:int) = let ab = a*b if b<>0 && ab/b <> a then None // overflow else Some(ab) let safediv(a:int, b:int) = if b = 0 then None else let quotient = a/b if quotient<0 && a<0 && b<0 then None // overflow, eg -128/-1 for bytes else Some(quotient) let final_result = safeadd(8,4) |> bind (fun x -> safediv(x,3)) // 12/3 |> bind (fun x -> safeadd(x,-14)) // 4-4 |> map (fun x -> x*0) |> bind (fun x -> safediv(1,x));; printfn "result is %A" final_result;; (* Unlike map, the bind operator must be applied if the function to be applied to the enclosed value also returns an Option. bind and map can mix: we used map for x*0 since that can't cause overflow. For emphasis: the difference between map and bind is that (Some 3) |> map (fun x -> x+2) returns (Some 5) (Some 3) |> bind (fun x -> Some(x+2)) also returns (Some 5) However, (Some 3) |> map (fun x -> Some(x+2)) returns (Some (Some 5)) To avoid creating a nested Option when applying a function that creates an option, call bind indead of map. An alternative to bind is to call Option.flatten: flatten (Some (Some 5)) returns (Some 5). map composed with flatten is equivalent to bind. If either map or bind is applied to None, it will always return None. The bind operator is also called "flatMap" (Java) or "and_then" (Rust). *) let and_then = Option.bind; // creates an alias (* The difference between None and the null pointer: There is one operation on Options that you should generally avoid, even though it's tempting: Option.get Option.get (Some 3): returns 3 Option.get None: THROWS AN EXCEPTION (crashes program). If you call get on Options, then soon or later None will start to take on many of the characteristics of the null pointer that makes it so costly. Calling get on a None is like dereferencing a null pointer. Of course, it's "safe" if you write an if-statement ... if (Option.isSome x) then printfn "%A" (Option.get x) This is similar to writing in Java: if (x!=null) System.out.println(x.value()); But the point is that there's no requirement that such checks are made, and sometimes the boolean used in checks isn't so obvious. That's why the null pointer and exception handling are allowing too many software errors to go undetected. The Option type does a better job at FORCING the programmer to confront the fact that errors may occurr. One function that's a bit safer than Option.get is Option.defaultValue, which requires you to supply a value to return if the option is None: *) let get_or = Option.defaultValue; // shorter alias for function. let thestring = get_or "" None; // returns "" if there's nothing to get. // But get_or is only appropriate when there is a reasonable default. (* Integrating monadic error handling with exceptions. One problem with exceptions is that there's often no way to catch and handle an exception locally. *) let x = "this is not a number"; //let y = int(x); // tries to parse x into a number - throws exception. (* We'd have to eventually wrap the call inside a try-with block try (int x) with | e -> ... Why can't the int function catch and handle the exception locally? Because it's obliged to return an integer. But any integer returned would be wrong. We also can't just place a try around every call to the function because the calling function may well face the same problem: let area() = try let radius = float(Console.ReadLine()); Math.PI * radius * radius with | exception1 -> ???? This function has no choice but to propagate the exception, or throw its own exception. Functions are supposed to ENCAPSULTE an algorithm, but the error-handling aspect of the algorithm ESCAPES the encapsulation. A function that throws an exception without catching it is "passing the buck." Error handling becomes a global instead of a local problem. With Option however, we're forced to deal with the problem on the spot. The fact that int(x), float(x), etc throw exceptions is built-into .Net and can't be changed. But with Option we can now rewrite these functions that always catch the exception locally and return None: *) let parse_int (s:string) = try Some(int(s)) with | _ -> None // or, more generally: let on_behalf f = try Some(f()) with | _ -> None let argv = Environment.GetCommandLineArgs(); // command-line arguments on_behalf (fun () -> float (argv[2])) |> map (fun radius -> Math.PI * radius * radius) |> iter (fun area -> printfn "area is %f" area) // the dummy lambda in on_behalf delays the evaluation of the expression // so any exceptions (array index out of bounds, or can't convert to float, etc) // can be caught and None returned instead. // here's a more difficult example: on_behalf (fun () -> int(argv[2])) |> and_then(fun a -> printfn "got %d" a on_behalf (fun () -> int(argv[3])) |> and_then(fun b -> printfn "got %d" b safediv(a,b))) |> iter (fun q -> printfn "quotient = %d" q);; // here are some more operations you can call on Option types: let if_else some_func none_func = function | Some x -> some_func x | None -> none_func();; // some_func, nonefunc should return values of the same type. (on_behalf (fun () -> int(argv[2])), on_behalf (fun () -> int(argv[3]))) ||> map2 (fun a b -> safediv(a,b)) // map2 applies to a pair of monads |> flatten // no bind2, so need to flatten |> if_else (fun q -> printfn "quotient = %d" q) (fun () -> printfn "no result due to errors");; (* The monadic operators, both the built-in ones like map, iter, bind, as well as additional ones can define ourselves, essentially defines a new language above F#. The language operates on optional values that always carry with them the possibility that errors have occurred. This new language, with its own constructs such as if_else, on_behalf, etc, allows us to write programs that are always error-aware. They should never crash. *)