We'll start out with a standard pattern I've observed in a normal C# application. Let's say there is a function that should be called, but only if a number of other validation functions first succeed. The C# code could look as follows (this code could be written better but it is just an example):
public static bool SaveRecord(int input) {
var valid = IsGreaterThanZero(input);
if (valid) {
valid = IsLessThanTen(input);
}
if (valid) {
valid = EqualsFour(input);
}
if (valid) {
SaveRecord(input);
}
return valid
}
In this example we want to call SaveRecord, but only depending on the results of some other functions. Instead of branching on the valid boolean we could use exceptions, or we could put those validation calls in one line, but I'm going to argue for a better way. First let's write some code in F#. I'm a fan of Scott Wlaschin's railway programming, where program execution shortcuts if there is an error instead of using exceptions. To do this we need a type like so:
type Response =
| Success of int
| Failure of string
Instead of returning simple types our function inputs and outputs can be wrapped with this Response type. By using this response type our functions will have more well defined inputs and outputs which can lead to more readable programs as we will see below. So, let's re-implement the above function in F# using this response type (warning: ugly code below!):
let yuck input =
match isGreaterThanZero input with
| Failure f -> Failure f
| Success a -> match isLessThanTen a with
| Failure f -> Failure f
| Success b -> match equalsFour b with
| Failure f -> Failure f
| Success c -> saveRecord c
As you can see that looks terrible. After every response we have to check whether not the response is valid before continuing, but luckily the language provides a way to clean it up. I'll show you step by step. First, let's create a function called bind that will help remove some duplicate logic in this function.
let bind m f =
match m with
| Failure f -> Failure f
| Success a -> f a
This bind function takes a response type and a function, then simply propagates the failure if it's a failure or if it's a success it calls the function with the unwrapped value as its parameter. This serves the purpose of removing the nested match statements above and helps readability.
let withBind input =
let result1 = bind (Success input) isGreaterThanZero
let result2 = bind result1 isLessThanTen
let result3 = bind result2 equalsFour
bind result3 saveRecord
This is much more readable than the first function but we can take it a step further. We can use computation expressions in F# by building a type with the bind function we created that looks as follows:
type ValidationBuilder() =
member this.Bind(m, f) = bind m f
member this.Return(x) = Success x
member this.ReturnFrom(x) = x
member this.Zero(x) = Success x
let validation = new ValidationBuilder()
With this type we can write a function that is cleaner than before.
let withContinuation input =
validation {
let! result1 = isGreaterThanZero input
let! result2 = isLessThanTen result1
let! result3 = equalsFour result2
return! (saveRecord result3)
}
The computation expression allows us to build in the continuation logic so it doesn't have to be repeated everywhere in our application. The ! operator automatically calls the bind function, unwraps the value and continues on unless there is a failure in which case none of the other lines are executed. One other option we could use here is an infix operator instead of a computation expression. These can be confusing if used too often but here is how we could change the function using an infix operator.
let (>>=) m f =
match m with
| Failure f -> Failure f
| Success a -> f a
The infix operator works like the bind function and when we use it in our original function this is the result.
let withInfixOperator input =
input
|> isGreaterThanZero
>>= isLessThanTen
>>= equalsFour
>>= saveRecord
This is the most readable of all the options and shows the power of F#. See, monads aren't scary! If used correctly your programs can contain mostly program logic and are very easy to read as long as the basic concepts are well understood.
Monads are very handy in functional programming paradigms to handle the side effects and to build clean code. They make operations which otherwise are complex such as chaining computations much easier. When engaging with these ideas, an ending for your thinking, well-polished thesis, is very significant. A Thesis Proofreading Service must therefore be useful in fine tuning your academic work to achieve more precise explanations.
ReplyDelete