In C#, our preferred way to define a builder is using a fluent syntax. Whether it is setting up the system under test in our specifications by defining how we want external calls to be faked, setting up complex test data or defining the steps to be executed in an operation with automatic compensation in case of an exception.
In F#, this works, but feels weird, because a fluent syntax typically uses classes and/or extension methods and there are a lot of
So we experimented with a different style to define the above-mentioned things, and we found a way that better matches our taste of simple code (yes, this post is about taste 🙂 ).
A fluent syntax in C#
A fluent syntax in C# looks normally something like this:
var result = Builder .Run(() => this.RunFirstThing(firstData)) .Run(() => this.RunSecondThing(secondData)) .Save(dataToBeSaved) .Execute()
Builder is used to define the steps (
Save) that are afterwards executed by
Execute. Internally, the execute method typically wraps the passed actions or functions with some common code, like error handling and logging. Using this builder prevents code from being repeated and still allows a flexible usage.
Modelling the steps with discriminated unions
In F#, we switched to a different design for such builders. We use a discriminated union to model the steps and pass them to a function for execution:
type Data = ... type Steps = | Run of unit -> unit | Save of Data module Runner = let saveData data = ... let execute steps = for step in steps do match step with | Run action -> action () | Save data -> saveData data Runner.execute [ Run (runFirstThing firstData) Run (runSecondThing secondData) Save dataToBeSaved ]
Note that we can specify for every step what data it needs to be executed in the discriminated union case.
Adding error handling and logging
The common functionality is added in the
execute function – as it is in C#:
module Runner = let saveData data = ... let execute steps = logStart () try for step in steps do match step with | Run action -> action () | Save data -> saveData data logSuccess () with | _ -> logError () rollback ()
If we want to implement meaningful logging and rollback function, we need some more data to be passed to the runner:
type Data = ... type Saver = ... type Steps = | Run of unit -> unit | Save of (Saver: Saver*Data) module Runner = let saveData saver data = ... let execute operationId logger sendRollbackMessage steps = logger.LogStart operationId try for step in steps do match step with | Run action -> action () | Save saver, data -> saveData saver data logger.LogSuccess operationId with | _ as e -> logger.LogError operationId e sendRollbackMessage operationId
Let’s assume that
operationIdis an unique identifier for the operation executed
loggeris a dependency that provides the logging functionality and knows where to write the logs
sendRollbackMessageis a dependency that writes a message to a service bus that the operation has to be rolled back.
How we roll back operations in real is a topic of its own.
saveris a function that knows how to save the data
Dependencies and data that are shared by most steps are passed directly into the
execute function. Dependencies and data used by single steps are better passed to the step itself.
And our F# runner is finished and can be used like this:
Runner.execute operationId logger sendRollbackMessage [ Run (runFirstThing firstData) Run (runSecondThing secondData) Save (saver, dataToBeSaved) ]
Almost as easy to use as the fluent syntax in C#, but with less syntax noise. I write “almost” because IntelliSense can’t quite help as much as with a fluent syntax. But it reads better in my opinion.
A side note on computation expressions
I found a couple of examples that handle such scenarios with computation expressions and custom operations. We are not using this approach because our runners are nested in
async workflows, I find nested computation expressions a pain.
You know a better way to achieve the same? Please let me know in the comments!!
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).
Piping with the |> operator is really the F# equivalent of fluent syntax; implemented in that style there’d be a Runner module with a number of functions to start a sequence, run a step and then save off the result, used something like
Runner.start operationId logger sendRollbackMessage
|> Runner.run runFirstThing firstData
|> Runner.run runSecondThing secondData
|> Runner.save saver dataToBeSaved
Thanks for your feedback!
But how could I execute something at the end of the runner executing (like the `logger.LogSuccess`) or catch exceptions?
[…] Find the next post in this series here. […]
One way of doing a fluent interface with the logging and error handling is to use a lightweight version of the State monad. It’s just a minor refactoring of the existing code, like
// stub types here
type Data =
| PlaceHolder of Object
type Saver = Data -> unit
type OperationId =
| Id of Object // placeholder type
type ILogger =
abstract LogStart : OperationId -> unit
abstract LogSuccess : OperationId -> unit
abstract LogError : OperationId -> Exception -> unit
// end of stubs
type RunState =
| Context of OperationId * ILogger * (OperationId -> unit)
| Failure // passthrough state
module Runner =
let private saveData (saver:Saver) data =
let private execute action runState =
match runState with
| Failure -> Failure
| Context (op, log, rollback) ->
| _ as e ->
log.LogError op e
let start operationId logger sendRollbackMessage =
Context (operationId, logger, sendRollbackMessage)
let run action runState =
execute action runState
let save saver data runState =
execute (fun () -> saveData saver data)
Many thanks for the clarification.
I like your proposal.
I’m thinking about a different implementation, however:
Instead of executing the steps right away, I collect them and execute them on the last call to the run (`save` in the example). That’s where the success logging should happen (only once).
Alternatively, just move the success logging to the `save` function. The “client” won’t forget about it because otherwise, the return value has to be ignored. The second option looks simpler and more flexible to me.
Yes, that’s another entirely valid way of doing things —
Runner.start (unit -> RunState) almost as above, but with an extra empty queue in the Context payload;
Runner.run and Runner.save (RunState -> RunState) just add actions i.e. (unit -> unit functions) to that queue (Runner.save creating a closure as above), returning a Context with that updated queue;
and a final closing Runner.execute (RunState -> unit) takes the queue and associated data and runs it inside the try/with, and then tidies up, without needing to return anything.
[…] the previous blog post “bye-bye fluent syntax”, I wrote about a way how to implement an equivalent to a fluent syntax in F#, without classes and […]