The application we are developing – it’s TimeRocket, just in case you forgot 😉 – has quite a lot of code that looks something like this (pseudo-code):
- given is an identifier
- load the data represented by the identifier if it exists
- if the data could be loaded, validate if the desired action can be executed
- if it can then continue
- report back success or what failed
In C#, we often resort to exceptions because otherwise, we get a pyramid of doom.
So it’s time for one of the main reasons why we like F#: computation expressions.
There is some excellent content by Scott Wlaschin that explains computation expressions and the pyramid of doom way better than I can. So please, take a look at them before continuing:
Ah, you found your way back!
There is a great library that provides a lot of different computation expressions: FsToolkit.ErrorHandling.
Our code looks then something like this (simplified):
let addValueOnDimension
queryActivityStructureEvents // dependency to get data from the storage
activityStructureId // identifies the activity structure to extend
dimensionId // the dimension on which we want to add a value
value = // the value to add on the dimension
// the asyncResult computation expression handles Async and Result
// in the background so that we can focus on the logic here
asyncResult {
let! activityStructure =
activityStructureId
// event-sourcing: get all events for the activity structure
|> queryActivityStructureEvents
// project the events, may result in Option.None
|> Async.map ActivityStructure.projectSingle
// we require the activity structure
|> AsyncResult.requireSome "activity structure not found"
// since this code is executed, we found the activity structure
let! dimension =
activityStructure
// let's find the dimension, may result in Option.None
|> ActivityStructure.tryFindDimension dimensionId
// only continue if we found the dimension
|> Result.requireSome "dimension not found"
// we found the dimension, let's continue
do! dimension.Values
// check that the value is not already present
|> List.tryFind (fun v -> v.ValueId = value.ValueId)
// only continue if the value is not yet present
|> Result.requireNone "value already defined on dimension"
// execute code, we only get here if all the checks succeeded
// otherwise an Error with an error message is returned
addValue ...
}
With computation expressions for Option, Result, Async, Task and all combinations of them, it is much simpler to write validation and error handling code in F# than it is in C#.
Computation expressions alone help us to get closer to all three goals we set for ourselves when starting the journey to F#:
- reduced effort to create business value: the code is much simpler and therefore written faster, and reasoning about the code is simpler, too.
- reduce mental load: no pyramid of doom, business logic doesn’t get distracted by error handling
- less defects: defects have a much harder time to hide themselves in this simple code, compared to the C# we wrote before.
Happy “happy path”-coding everybody!
Find the next post in this series here.
Find all blog posts about our journey to F# here.
This blog post is made possible with the support of Time Rocket, the product this journey is all about. Take a look (German only).
[…] Find the next post in this series here. […]