As a followup of the previous post, I would like to adjust the last example (using some comments' suggestions) and introduce a first basic F# implementation, which will allow to illustrate Railway Oriented Programming.
Continuation implementation revisited
As suggested in the comments of the previous post, the continuation implementation would be even nicer with a more fluent interface (you know, like Linq API). Then, the CartController implementation would be:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: |
|
We could improve readability using function instead of lambdas.
To allow this fluent interface, I just modified the Either construct to return something instead of void
for ContinueWith
method. It returns a new Either with the same type for the second parameter. Depending on the result of the current Either, it continues with the one returned by the lambda given in parameter (the Left), else it switches to the Right. This switch mecanism can be viewed as a rail switch: you can continue "straight" (Left) OR you change to the other way (Right). For this reason, it is also known as Railway Oriented Programming (you will find great illustration with railway switches schemas + a link to some code in F# and C# also). By the way, I forgot to mention that Either
naming come from Haskell, it is a Monad (yes I said the M-word!). But I really like the metaphor with railway switches to explain that.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33: 34: 35: 36: 37: 38: 39: 40: 41: 42: 43: 44: 45: 46: 47: 48: 49: 50: |
|
My feelind is that Either is still quite (too much?!) verbose in object oriented language. Also, if you have a look at the (fake) implementation of Cart or ProductStock, you will see that it is also quite verbose to declare "go left" or "go right" as return value.
That's why, now, I propose you to switch to F# to see the magic happen :).
A first basic F# implementation
In this first F# implementation, I stick with the old continuation implementation shown at the end of the previous post (i.e without fluent interface), we will have something even nicer with F# in the next post.
The Either construct is called Result in this case:
1:
|
|
Yes, it is just one line! That's part of the magic with F# :). We still have to implement the ContinueWith
logic, but you will see it is not harder. I can even put the whole code here since it is so concise. It starts with the (immutable by default!) types I use:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: 24: 25: 26: 27: 28: 29: 30: |
|
Then I can declare the domain logic (equiv. of Cart and ProductStock classes)
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: |
|
Then I have some fake implementations of infrastructure (equiv. of ICarts and IProductStocks implementation):
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: |
|
And in the end, the controller orchestrates the whole:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: |
|
Some cool things to note:
- I do not hide anything from you, the whole F# code is there. It is far less verbose, without being cryptic IMHO. If you are not used to F#, it could be a little cryptic, but I highly encourage you to learn functional languages, I feel it is much easier than the whole OOP languages, patterns and practices. Scott Wlaschin explains more in depth the conciseness power.
- F# forces you to layer things in the order they are used, so it enforces dependencies' direction of an onion/hexagonal architecture: first bits of code are the core, last bits are the infrastracture and the shell of the application.
- I did not use any injection in the domain functions, on the opposite of what we are used to in OOP (like in CartService and ProductStockService in the previous post). It was already there in the previous OOP continuation implementation. Orchestration of infrastructure call (with side-effects) and domain logic is done in the shell. Both approches are hexagonal/onion architecture, but only the one without injection is pure functional architecture, i.e functional core (domain without side-effects = inner layer of the onion), imperative shell (side-effects + glue the whole = outer layer of the onion).
- And last, can you see the pattern with
function | Success ... | Failure ...
(using a shortened syntax of a pattern matching)? It will help us for the next post :).
Next step
From this first F# implementation, we will be able to evolve towards an even nicer implementation, removing the pattern we just spotted. It will explain by example what is a Monad. And I will also show you how to use Computation Expression in F# to have a final version I really like.
| Success of 'a
| Failure of string
Full name: functionalarchitecturepart.Result<_>
union case Result.Failure: string -> Result<'a>
--------------------
active recognizer Failure: exn -> string option
Full name: Microsoft.FSharp.Core.Operators.( |Failure|_| )
val string : value:'T -> string
Full name: Microsoft.FSharp.Core.Operators.string
--------------------
type string = System.String
Full name: Microsoft.FSharp.Core.string
val int : value:'T -> int (requires member op_Explicit)
Full name: Microsoft.FSharp.Core.Operators.int
--------------------
type int = int32
Full name: Microsoft.FSharp.Core.int
--------------------
type int<'Measure> = int
Full name: Microsoft.FSharp.Core.int<_>
Full name: Microsoft.FSharp.Collections.list<_>
module List
from Microsoft.FSharp.Collections
--------------------
type List<'T> =
| ( [] )
| ( :: ) of Head: 'T * Tail: 'T list
interface IEnumerable
interface IEnumerable<'T>
member GetSlice : startIndex:int option * endIndex:int option -> 'T list
member Head : 'T
member IsEmpty : bool
member Item : index:int -> 'T with get
member Length : int
member Tail : 'T list
static member Cons : head:'T * tail:'T list -> 'T list
static member Empty : 'T list
Full name: Microsoft.FSharp.Collections.List<_>
Full name: Microsoft.FSharp.Collections.List.tryFind
Full name: Microsoft.FSharp.Collections.List.except
Full name: Microsoft.FSharp.Core.Operators.id