Put your F# domain on a diet
In my previous article I wrote some uninspired but effective code that illustrates how custom types can be easily added to your domain to replace strings, requiring some additional type declaration code but resulting in a more robust solution with less validation and error handling code.
Trust me, it’s a thing
Creating such types and defining your domain with them instead of strings is sometimes referred to as designing your domain with types. Type here implies a type that you create for that specific thing in your domain, complete with the validation that prevents it from ever existing with invalid content.
Your last name is not any string
This sort of design means you don’t define last name as a string, because strings that are empty or contain control characters are never valid names. So you create a type specific for last name that contains that rule embedded in it.
Declaring such a type carries some verbosity cost of course, so you’ll probably reuse that type for first name as well, those usually share the same validation. The point of designing with types is not to create as many types as possible, but rather to make it impossible for your domain entities to be populated with invalid content without relying on validation logic defined outside of your domain. In other words, to make illegal states unrepresentable.
There’s a cost, but it’s not a net cost, it’s more like a trade. Think about it this way: for your app to function correctly when declaring text fields as strings, you’ll be doing that validation somewhere else, likely requiring similar amount of code.
What’s more, when debugging illegal states you may be tempted to add extra error handling code to swipe the problem under the rug. No judgement, we’ve all done it, errors don’t always happen at a convenient time for you to perform a complete differential diagnosis.
Time for domain fitness
So this domain bloat is worth it, but that doesn’t mean we should stand idle while our once-elegant domains get fatter and fatter, especially when it can be avoided.
While designing with types will never ever enjoy the brevity of designing with strings, there’s ways to dramatically reduce the amount of code that it requires. Judge for yourself, here’s a type from my previous article:
// Object oriented style Email type with embedded validation type Email private (s:string) = class end with static member Validate = function | s when String.IsNullOrWhiteSpace s -> Error "input is empty" | s when Regex.IsMatch(s, "^[^@]+@\w+.\w+$") -> Ok (Email(s)) | _ -> Error "invalid email" member x.Value = s
And here’s the same type defined with
type Email = private Email of Text interface TextBlock with member _.Validate = fun s -> Regex.IsMatch(s, "^[^@]+@\w+.\w+$") => InvalidEmail
There’s a lot to unpack here, but the point is clear, the code required to create types with embedded validation can be significantly reduced.
Let’s dissect that Email type 🧐
While I won’t deep dive into
FSharp.ValidationBlocks in this article, I believe it’s important to describe everything that’s going on with that
(* 1 *) type Email = private Email of Text (* 2 *) // interface TextBlock (* 2' *) interface IBlock<string, TextError> (* 3 *) with member _.Validate = (* 4 *) // fun s -> Regex.IsMatch(s, "^[^@]+@\w+.\w+$") => InvalidEmail (* 4' *) fun s -> [if Regex.IsMatch(s, "^[^@]+@\w+.\w+$") then InvalidEmail]
- This interface identifies the
TextError, which is just an enumeration of all possible text validation errors somewhere in your project. This interface has little to do with OO interfaces, it’s just here for declarative purposes. In real world examples, I abbreviate this interface to just
IBlock<string, TextError>because it’s used to declare all blocks of string.
- In addition to identifying the
'a -> 'error list, or in our concrete example:
string -> TextError list.
- This is the actual validation rule, it’s a trivial function that enumerates a list of simple predicates and the errors that each yields when true. There’s operators to slightly simplify this syntax but they’re completely optional.
Wait, what? How?
I’m aware the above code even with line by line explanations may raise more questions than it answers:
- How do you create an
- How do you access its value?
- What’s the
Email of Text?
- Where did the empty string check go?
All valid questions so let’s go through them one by one:
How do you create an Email block?
Block.validate "email@example.com"resulting in a
How do you access its value?
You can access the content of an
Block.value emailor simply using the (experimental) operator
Email of Text?
Blocks are built on top of other blocks so that hey only declare the validation that’s unique to the block itself, so here
Textis just another block type to which
Text’s validation, without any of the object oriented inheritance antics.
Where did the empty string check go?
There’s no need to explicitly check for empty strings because
Textcan never be empty
Enough explanations, let me see more code
Patience grasshopper, there’s a lot here to digest. Introducing a completely different but ultimately leaner way to designing with types in F# is a domain fitness journey that takes time but will inevitably make your domain both more enjoyable to create and to maintain.
In the next article I go through a complete example but in the meantime remember, an elegant domain isn’t just nicer to look at, it’s also healthier. The less boilerplate code it has, the easier it is to spot issues.
Feedback & more
If you enjoyed this article or have any comments please consider retweeting or replying to this article’s tweet, it’s very appreciated.