Gong back to @nickwalt’s earlier question about a more DDD perspective on “narrower types” in Elm, here are 4 concepts I use all the time. Some of them are well-known guidelines viewed from a more domain-oriented point of view.
Avoiding primitives
Using narrower types in our functions usually means using more domain-specific types and avoiding primitives (OO would call this “avoiding primitive obsession”). One example from my own code is that I avoid using raw numbers and instead create custom types such as Dollar
that represent different quantities. I have a whole talk on the topic
Restricting operations
Wrapping numbers in custom types is often seen as inconvenient because then you have to re-implement arithmetic operations for that type. This can actually be a feature because in many domains not all arithmetic operations make sense.
For example multiplying dollars times each other might be an invalid operation but multiplying distances could be valid. By creating a custom type, you are able to have a “narrower” set of functions available, all of which match the operations valid in your domain.
Your modeling should match your domain
“Making impossible states impossible” is a common refrain in the Elm community. People often think of it in terms of correctness. I tend to view at as making your your model (in the DDD sense, i.e. the types) accurately describes you domain.
You can think of your domain and types as sets. If your problem domain has 5 possible values but the types you use to model it have 10, then your model is not very accurate representation of reality.
Ideally, your domain and the types you use to model it are the same set. A type system like Elm or F#'s that allows you to both AND and OR types together is really powerful because it allows you to more easily reach this goal than a traditional system that only allows ANDing values.
Conversions to narrow types
One of the big realizations I had when reading Parse; don’t validate is that parsing isn’t just about strings. Instead, it’s about turning a broader type into a narrower type (with potential errors since not all values in the broad type can be converted). Parsing functions usually have a signature like:
parse : broaderType -> Result Error narrowerType
This is probably the most concise way of expressing the concept of “type narrowing” as a function signature. This generally happens near boundaries of sub-systems (DDD would call these contexts).
One could even transform data in several passes, with the parsed output at each step becoming the raw input of the next step. The types get narrower and narrower at every step and the pipeline acts as a funnel.
For example, a String
could get turned into a Json.Decode.Value
, which is a narrower type. This in turn might be turned into a UserSubmission
value which is narrower still, and finally this could be converted into a User
that is narrowest of all.