Less is more. Wrapping Elm primitives in custom types restricts your available operations and that’s a good thing. Ideally, you only expose operations that make sense in your problem domain.
Inspired by a discussion on narrowing types from the Elm discourse.
Units of measure
I commonly recommend avoiding raw numbers in Elm code and instead wrapping them in a custom type describing what sort of quantity a number represents. This technique helps prevent common bugs like argument order bugs or conversion errors. It also massively improves code readability.
Implementing such a type usually looks like this:
type Dollar = Dollar Float
Breaking math
So far, so good, but people quickly discover that you can’t do basic arithmetic with these values anymore.
-- results in a compiler error
(Dollar 5) * (Dollar 2)
So now we either have to unwrap/re-wrap the values inline every time we need to
do math, or we need to re-implement arithmetic in a Dollar
module.
module Dollar exposing (Dollar(..), multiply)
type Dollar = Dollar Float
multiply : Dollar -> Dollar -> Dollar
multiply (Dollar d1) (Dollar d2) =
Dollar (d1 * d2)
-- implement other arithmetic operations
This is tedious, and we have to re-implement the same 5 or 6 basic arithmetic operations for each type. Is this ceremony really worth it?
Thinking in domain terms
YAGNI (you ain’t gonna need it) is a common programmer aphorism, and it applies here. Do we really need all the basic arithmetic operations to work with dollar amounts?
No! In fact, some arithmetic operations are nonsensical. In most application domains, dollar values cannot be multiplied by other dollar amounts (this would result in a value with the unit of “dollars squared”*). Think about your domain and which operations are actually valid there. It’s usually a lot fewer than you think.
From this perspective, the fact that basic arithmetic doesn’t work on custom types is actually a benefit. It gives you a clean slate to write a narrower set of functions that only implement the operations that are valuable in your domain.
*When doing math on quantities, you apply the operations on both the numbers the units of measure. This is why calculating area of a 4ft by 2ft rectangle gives you values in square feet, not feet. Following this rule can lead to complex compound units such as (kilograms * meters) / seconds squared which is how force is defined in physics.
As programmers, we tend to play fast and loose with numbers. We can use this rule as a heuristic to help us avoid bad operations in our code. If I’m multiplying dollars times dollars, I get a quantity back in dollars squared. Is this a valid quantity in my application? Unless I’m doing some fancy financial modeling, probably not. This hints that I should not be multiplying these two particular numbers. Potential bug averted!
Less constrained domains
There are less constrained domains where we do want to be able to use just about every arithmetic operation. We do want to work with all those compound quantities. A physics simulation would be an example of such domain.
In cases like this, it can be helpful to pull in a library such as the excellent ianmackenzie/elm-units.
Not just types
Reducing the possible operations to just domain ones is a valuable side-effect of avoiding primitive obsession. It shows up even outside of statically-typed functional languages.
Consider the following scenario in Ruby
class Dollar < Struct.new(:cents)
def +(other)
if other.kind_of? Dollar
Dollar.new(self.cents + other.cents)
else
raise "Adding dollars and #{other.class} is an invalid operation"
end
end
end
Elm in the Spring talk
Curious to learn more about modeling quantities and limiting the surface area of their interfaces? I gave a whole talk at Elm in the Spring 2019 digging into various techniques.