Yan Cui
I help clients go faster for less using serverless technologies.
This article is brought to you by
Don’t reinvent the patterns. Catalyst gives you consistent APIs for messaging, data, and workflow with key microservice patterns like circuit-breakers and retries for free.
NOTE : read the rest of the series, or check out the source code.
If you enjoy reading these exercises then please buy Crista’s book to support her work.
Following on from the last post, we will look at The One style today.
Style 9 – The One
You may also know this style as Monads, which many consider to be a scary word… That said, I know a few smart people who have done excellent talks to explain Monads in a way that’s easy to understand, check out these links:
- Scott Wlashcin – Dr Frankenfunctor and the Monadster
- Andrea Magnorsky – Computation expression in context
and this post is probably the best of the lot, especially if you enjoy cartoon drawings!
Constraints
- Existence of an abstraction to which values can be converted
- This abstraction provides operations to:
- wrap around values, so that they become the abstraction
- bind itself to functions, so to establish sequences of functions
- unwrap the value, to examine the final result
- Larger problem is solved as a pipeline of functions bound together, with unwrapping happening at the end
- Particularly for The One style, the bind operation simply calls the given function, giving it the value that it holds, and holds on to the returned value
Version 1 (simple bind)
In terms of a port from Crista’s example, most of code is very similar to the Pipeline style, except their return values have to be wrapped into another abstraction.
I thought about this and couldn’t come up with a really meaningful abstraction, so I settled on a single-case Result type:
Next, we need to define the ‘bind’ operator (which, the convention is to use >>=):
the rest is pretty straight forward (i.e. copy from Pipeline style, add |> Result to end of each function):
Before we move on though, I wanna bring your attention to the removeStopWords function briefly:
If you recall, readFile now returns a Result<string> so in order to split the wrapped string we’ll need to unbox it first. Since our Result type is single-cased, we can kinda cheat by using pattern matching to extract the wrapped string value out and bind it to the raw value:
Now, we want to chain the functions together using our bind:
Oh no! What’s happening here?!?
Ah, the compiler is telling us that ‘bind’ is expecting a continuation that returns a value wrapped in the Result type but printMe returns unit instead.
That’s a bummer, so we can either rewrite printMe to be compliant of such requirement. Or, we can use another concept that’s commonly used as a ‘lift’:
A lift is simply a function that takes a function that returns an unwrapped value, and returns a modified version that returns a wrapped value instead.
So with a tiny change, we can make everything work now:
But but but, this is F#, and we have computation expressions, so we can do better than that!
(if you’re totally new to the idea of computation expressions, then I recommend reading at least the first few posts in Scott’s excellent series before proceeding)
Version 2 (computation expressions)
Have you read Scott’s introductory posts on computation expressions? Have a basic understanding of how they work?
Good, then let’s continue.
Here is a very simple CE that works with the Result type:
and now we can use it to refactor the removeStopWords function:
Here, return! is translated to the ReturnFrom method in TheOneBuilder and unwraps the Result<string> for us so we don’t need to manually unwrap it with pattern matching.
To chain the functions together, we no longer have to use bind and can instead let the CE deal with unwrapping values for us:
Also, notice that we no longer have to ‘lift’ the printMe function.
aside : you might also noticed that I’m shadowing previous instances of text, words and wordFreqs as I go. It’s not necessary, and many people would have preferred text’, text’’, etc. instead.
I think in this particular case shadowing actually helps me prevent the accidental misuse of discarded values. E.g. the following would be a bug (and I’ve made this type of mistakes in the past..)
theOne {
let! text = readFile ”p & p”
let! text’ = filterChars text
let! text’’ = normalize text // should be text’ instead
…
}
That said, I personally think >>= is still a very useful combinator, and would prefer to write the above as:
You can find the source code for this exercise here (v1) and here (v2).
Whenever you’re ready, here are 3 ways I can help you:
- Production-Ready Serverless: Join 20+ AWS Heroes & Community Builders and 1000+ other students in levelling up your serverless game. This is your one-stop shop for quickly levelling up your serverless skills.
- I help clients launch product ideas, improve their development processes and upskill their teams. If you’d like to work together, then let’s get in touch.
- Join my community on Discord, ask questions, and join the discussion on all things AWS and Serverless.