When I learned about "Advanced Functional Programming with Elixir" by Joseph Koski (PragProg publishing) - detailing building a miniature theme park application (a favorite gaming genre of mine) - I rushed to buy it.
After all, Elixir is a wonderful FP language I enjoyed coding in the past.
But there's a twist - I'm reimplementing the system in F#, using everything I know about F# to make the domain safer and correct!
For all my love for Elixir, F# is much better at leading the developer into the "pit of success."
As with all coding posts, I suggest you checkout the repo and take a look at the code while reading.
Grokking our domain (covering original book's chapter 1)
As is customary, and wise, we start the journey with the domain entities: Ride
, FreePass
, and Patrons
(which are the park guests, as per the ubiquitous language for our domain).
The book starts with Ride
s.
The author immediately shows good software design by declaring it as a structure, an Elixir construct that takes specific shape, but only allowing instantiation via a constructor.
I was surprised that though Elixir does feature private functions, the author made the constructor public, defeating the purpose of only allowing instansiation via it.
In the F# implementation, I made the constructors private - no ad-hoc entity creation anywhere in the project by "rogue" code, no surprises!
But, with great control comes great compromises and technical challenges as we shall see momentairly.
Right off the bat, as I was modelling Ride
in F#, some key differences between Elixir and F# stood out:
- Elixir has succinct null-string verification but requires pattern-matching for empty/all-whitespace checks. F# actually doesn't have even that functionality built-in, but it's easy to implement by hand.
- No built-in UUID generation in Elixir, forcing integer IDs. F# wins this one with robust GUID/UUID generation tools right in the standard library.
- Erlang, on top of which Elixir is running, has symbols for strictly positive integers (which in math are called Naturals). F# doesn't ,but, again, it's easy to implement.
- Elixir supports passing default values to functions while F# doesn't.
As an aside, I favor F#'s approach: call functions with all required arguments, or let it fail.
I did however implement defaults, for the challenge.
(Keep in mind, default values grant type-, and runtime safety, allowing an object to be instasiated despite missing arguments, but might not make business sense!
Programming is, as is well known, an exercise in compromise.)
To make constructors private in F#, there are two approaches: FP-leaning and OO-leaning: see the series on constrained types by (the great) Scott Wlaschin.
For the Ride
type, I chose the FP-leaning approach purely for stylistic reasons.
Programming is also an exercise in style. 😜
To start an FP-leaning private type/constructor combo we first need to define the private type. The logic is impeccable, right?
// All preceding code elided
type Ride =
private { Id: Guid
Name: string
MinAge: int<yr>
MinHeight: int<cm>
WaitTime: int<s>
Online: RideStatus
Tags: RideTags list}
Can't compare apples to Lamborghinis
We can already see a feature of F#, not available out-of-the-box in any other programming language I'm aware of: Units-of-Measure (in short: UoM).
Notice those <yr>
, <cm>
, and <s>
? Those are tags, units, that qualify the numerical, integer, quantity they're attached to.
.Net has a few of them, the SI units, predefined, e.g., <s>
which is the SI unit second
. The others I had to qualify myself (look in Shared.fs
file in the Shared
folder to see how).
Thing is, much like a disciminated union, also known as tagged union, since each case acts as a tag, marking its corresponding data, UoM are light-weight tags too: an int<yr>
is type incompatible with int<s>
: if I were to do 5<yr> = 5<s>
, the compiler would throw a type error!
Can we have some privacy, please
Now, this code only defines the private type, but not the constructor I talked about.
The point to understand is that by qualifying the type as private
, the compiler will now allow only code in the type's enclosing module and/or namespace to access the type.
The way to create a constructor for the private type, which is public, is to create a sub-module, F# supports nesting modules as deep as you like, in the "main" module that will have a function that creates an instance, in the OO meaning of the word, F# types get compiled down to classes, of that type.
It's convention to name the sub-module after the type it creats, so I now declared module Ride
.
Now came the time for another contemplatation: how to pass the arguments, six of them (Name
, MinAge
, MinHeight
, WaitTime
, Online
, Tags
) to the constructor: either passing them as one tupled argument, or six curried ones, meant all of them would be positional: their place in the order of passed arguments will dictate what argument they are: the first is Name
, last is Tags
... for an arbitrary function that has that exact parameter list!
That would work, obviously, but given there are six such parameters, this risks mixing them up causing errors.
True, in this specific case, most of the arguments being tagged by UoM, any such mixups would be caught by the compiler as errors, but I'm lazy... re-typing a six argument list by hand over does not sound appealing!
I figured there must be a better way to pass the parameters to the function - and there is!
Luckily for me, the ID properity of each of the entities is generated via the Systenm.Guid.NewGuid()
method.
With that in mind I created a new type to provide the constructor function its arguments in a named fashion.
While I first hoped to pass an annonymous record to the constructor, e.g. {|Name = "myName"; MinAge = 8<yr>...|}
, functions in F# do not allow annonymous recrods as parameters, so a full-fledged type was in order.
What I came up was:
module Ride =
type RideConstructor =
{ Name: ContentfulString option
MinAge: Natural<yr> option
MinHeight: Natural<cm> option
WaitTime: Natural<s> option
Online: RideStatus option
Tags: RideTags list }
I'm gonna have my own types with private constructors and validations
I guess I now have some 'xplaining to do with regards to what are ConteftfulString
and Natural
which surely aren't built-in F# types.
I want my Ride
s to have meaningful names, e.g. not the empty string, nor an all whitespace name, and they definitely must not be null.
Same for the numerical value denoting age, heigt and wait time: neither of them can be a negative value, and actually, neither can be zero either! (Well, WaitTime
can, in theory, denoting an offline ride, as we shall see in an upcoming post about testing the domain!)
Both these types are also constrained types: private
with constructor and extractor functions and some logic to constrain the type instances' creation, to only allow values that are valid for the domain.
And yet, each of these types is also slightly different the Ride
too.
For ContentfulString
I used the OO-leaning construction of constrained types, simply because the OO-leaning way allows to attach the extractor to the type as a member, later allowing value extraction via the syntax aContentfulString.Value
:
type ContentfulString private (str) =
static member Create str =
if String.IsNullOrWhiteSpace str then
None
else
Some(ContentfulString str)
member _.Value = str
For Natural
, since it would be denoting a tagged UoM quantity, its definition needs to support that:
type Natural<[<Measure>] 'u> = private Natural of int<'u>
module Natural =
let create x =
if x > 0<_> then Some(Natural x) else None
let value (Natural x) = x
Failure is not an option (but in this case, it is)
Going back to the constructor type, you may be wondering, "but why are all the inputs to this record option
al"?
The answer has to do with the Natural
and ContenfulString
constructors.
If you look at the constructors of these two types you'll notice both output an option
al: Some<'T>
.
The reason is that when constructing those types the input might be invalid, e.g., a negative number for the Natural
constructor.
In that case, what should the constructor do? One way to go would be to raise an exception, but that's a very dramatic option, and also not very testable (the test would throw on any negative-path test, polluting the entire test suite's results!)
The better way, the F# idiomatic way, is to use the Option
type: when an invalid input is given return None
, otherwise, as noted earlier, return Some<'T>
.
"Sure", I hear you say, "that makes sense for the name and the numerical quantities that use your custom, constrained, types. But what about Online
and Tags
? Those are custom types, but they don't have constrained constructors, so, what's the deal?!"
Do, or do not, there is no default
The answer to that question is that by now I was sure I had the Ride
type figured out and it was time to create the constructor function.
As mentioned earlier, though I object, personally, to the notion of default values, I decided to implement them in this domain for the sheer challenge of it.
I also mentioned I created the RideConstructor
type to be able to pass named arguments to the function.
What this amounts to is:
// Within the `Ride` module
let create
({ Name = name
MinAge = minAge
MinHeight = minHeight
WaitTime = waitTime
Online = online
Tags = tags }: RideConstructor)
=
{ Id = Guid.NewGuid()
Name = defaultArg (Option.map (fun (n: ContentfulString) -> n.Value) name) "Generic ride!"
MinAge = defaultArg (Option.map Natural.value minAge) 8<yr>
MinHeight = defaultArg (Option.map Natural.value minHeight) 100<cm>
WaitTime =
if online = Some Offline then
0<s>
else
defaultArg (Option.map Natural.value waitTime) 60<s>
Online = defaultArg online Online
Tags = List.distinct tags }
Aside: I didn't have to supply the input's type, RideConstructor
, F#'s type inference engine would have infered it by its own, but I decided to add it as a hint for future readers (i.e., me).
Now it should be apparent why I made all the inputs to the constructor optional... as I said before, F# doesn't support default value to function's arguments at the function's call site.
What F# does have is the defaultArg
method, as part of its core library.
The type signature for this method is option<'T> -> 'T -> 'T
.
Let's deconstruct that: the first argument is a generic optional value, i.e., it can be of any inner type.
The second argument is a concrete value of the same type.
The outcome is a concrete value of the same type, too.
What that means is that, in essence, defaultArg
takes an optional, and a concrete value: if the optional exist, i.e., its a Some<'T>
, we get back the inner value of the Some
. If, however, we pass a None
, the method's output is the concrete value we passed as its second argument: the default value!
I didn't say it's hard creating default values in F#, just that it's not possible at function's call site. 😉
Since I wanted consistency in my parameters, and allowing default values to all parameters, like in the original book, I set Online
and Tags
as option
al as well, and used defaultArg
on them, too.
BTW, as I mentioned earlier, default values provide type safety, not business logic safety!
Consider the default name I assign: "Generic ride!". In the unfortunate event our ride constructor function (not yet implemented) is failing we'd have quite a lot of "Generic ride!"s.
Does it make sense? Nope! But that's what you get when you opt-out of failing fast!
It's the little touches that count
Maybe you also noticed that I employ List.distinct
on the tags list, or the algorithm for creating the waiting time?
Neither of these is in the original book, but I like it.
The first ensures there are no duplicate tags in the tag list provided to the constructor, no sense in that after all, and the second makes sure an offline ride has a 0 wait time... no one is waiting on an unoperational ride!
The private constructor conundrum
At this point I was sure I'm done with the Ride
type and can move on to the others. I used F#'s REPL (AKA fsi
) to create a few Ride
s and things seemed to work fine.
Or did they?
Everything was fine... until I needed to access an instance's fields in tests, at which point all hell broke loose!
It's not so much a mistake, as a limitation imposed by the fact that for a private type, only the module holding its defintion can access its fields!
Even an extractor function wouldn't work: its return type would be Ride -> Ride
, but Ride
is private!
Twins - like in the Schwarzenegger/De-Vito movie
This may surprise you, as in the examples from Scott Wlaschin's blog posted earlier, the extractor works fine.
The difference stems from Scott's example constrained types being "boxes" for inner values like String50 str
- when extracting, we want the str
content, not the String50
wrapper.
Our domain types, on the other hand, are the entirety of what the constructor produces. Extracting a Ride
simply returns another private Ride
which is what we need back!
The solution turned out to be intuitive but, admittedly, cumbersome: creating a public clone type, RideView
, and setting its fields to the original instance's values.
Programming is, it turns out, an exercise in creative workarounds. 🤯
The closer - Like Brenda Leigh Johnson
It's now time to bring the Ride
type to conclusion.
With the RideView
type being public, I could now write an extractor function that returns a type whose members I could access on demand, which is the final part missing before I can test my implementation for correctness.
In my Rides.fs
file I made a design decision to put RideView
after the Ride
type.
This has significance as F#'s compiler infers types that have the same shape in order from top to bottom, so when referring to types of the shape of Ride
/RideView
, unless explicitly annotated, inference will always infer RideView
, which is not what we want!
For instace, our create
function now has to be annotated on its return type:
let create
({ Name = name
MinAge = minAge
MinHeight = minHeight
WaitTime = waitTime
Online = online
Tags = tags }: RideConstructor)
// Notice the return type annotation
: Ride = ...
The reason is when creating I do want a Ride
, not RideView
, but without annotation as discussed, the type would have been infered incorrectly.
Lastly, we finally are ready to create the extractor function.
let value (ride: Ride) : RideView =
{ Id = ride.Id
Name = ride.Name
MinAge = ride.MinAge
MinHeight = ride.MinHeight
WaitTime = ride.WaitTime
Online = ride.Online
Tags = ride.Tags }
This time I didn't have to annotate the output, inference would have worked just fine. I did, however, have to annotate the input to let the compiler now I'm only willing to accept Ride
s to the extractor function... there's no sense in extracting a RideView
.
The body of the function is pretty self-explantory: create a record of the public RideView
type, a public clone of Ride
, and assign each of its members the corresponding value from the private Ride
... a 1-to-1 clone of the input, but public, with accessible members that would be testable.
All good things come to an end
With this, the Rides
module is complete: we have a private constructor to only allow Ride
s to be constructed by a trusted function, and a public extractor to allow testing.
It tooks some compromises and some work-arounds, but it is now done.
Mission - accomplished!
The FreePass
and Patrons
types follow the exact same logic, with the same constrained types (actually FreePass
takes valid Ride
s and .Net's native DateTime
... the constructor logic is straight-forward once you followed this post).
And so we finished our initial domain modelling according to what we know about the domain thus far.
Now remains the "little" matter of testing to make sure my implementations actually got the job right.
But that's for another post... 😝
Thanks for reading all this scroll of a post, hope you enjoyed, maybe even learned something new, and, of course, comments are welcome, especially if they're nice. 🤣
Top comments (0)