Fewer Operations on Custom Types is Valuable


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.

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

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?

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!


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.

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

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.




Source link

Latest articles

Related articles

Leave a reply

Please enter your comment!
Please enter your name here