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):
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
let! activityStructure =
// event-sourcing: get all events for the activity structure
// 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 =
// 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
// 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
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).