Ben, Joël and I recently built an Elm application over two investment days, with a focus on trying out a new (to us!) paradigm in Elm. In this application, we avoided type aliases and primitive types ( String , Int , etc.) in favor of product types.

One of our types was for currency. Let’s break down the three ways we could declare this:

-- primitive type (Int) type alias Player = { dollarsOnHand : Int , dollarsInBank : Int } -- type alias type alias Dollar = Int type alias Player = { dollarsOnHand : Dollar , dollarsInBank : Dollar } -- product type type Dollar = Dollar Int type alias Player = { dollarsOnHand : Dollar , dollarsInBank : Dollar }

Primitive obsession outlines the use of primitive types (often String , Int , etc.) instead of defining domain-specific types. In our case, the first Player uses the type Int to describe currency. In Elm, however, this is almost counter-intuitive, and relies on function names alone to convey intent.

Imagine the type signature Int -> Int -> Int ; what do those Int s represent? What operations could apply that take two Int s and return an Int ? The possibilities are endless, and within an application, can be quite confusing without looking at the function name.

This both reduces readability and increases the opportunity for bugs to sneak in.

Next, imagine Dollar -> Dollar -> Dollar or even Dollar -> Int -> Dollar . These tell a more compelling story. Even without the function name, my mind immediately fills in the first as likely describing addition or subtraction, and the second multiplication or division.

Finally, imagine Dollar -> Quantity -> Dollar . This might be a function to calculate the total cost given a specific number are purchased.

With what seems like a clear win, let’s move on to the next option: type aliases.

Type aliases allow developers to declare functions with more specific types as we did in the examples above; however, the compiler will still allow any appropriate type in! This means we have an improvement on the readability front (since the type signature can more accurately convey what’s happening), but the compiler still can’t protect us from applying values in the wrong order.

Let’s loop back to the Dollar -> Quantity -> Dollar example; with a type alias, any appropriately-typed value (an Int ) would be allowed, meaning the compiler reduces the signature to Int -> Int -> Int still.

I’ve been bitten by order-of-operations bugs that can be hidden because of mis-ordered arguments.

Imagine this buggy code ( fee and quantity are flipped in the list of arguments in calculateTotalPrice ):

type alias Dollar = Int type alias Quantity = Int type alias AdditionalFee = Dollar calculateTotalPrice : Dollar -> Quantity -> AdditionalFee -> Dollar calculateTotalPrice dollar fee quantity = -- quantity and fee are flipped! dollar * quantity + fee calculateTotalPrice 10 10 5 -- the code compiles, but the type signature doesn't help ensure correctness

This returns 60 : Int (10 * 5 + 10) instead of 105 : Int (10 * 10 + 5).

The order of arguments being flipped ( fee and quantity ) isn’t obvious to spot, but the compiler can help us here.

Let’s add product types for Dollar and Quantity , as well as the mathematical operations we’ll be using:

type Dollar = Dollar Int type Quantity = Quantity Int type alias AdditionalFee = Dollar multiply : Dollar -> Int -> Dollar multiply ( Dollar value ) quantity = Dollar <| value * quantity plus : Dollar -> Dollar -> Dollar plus ( Dollar a ) ( Dollar b ) = Dollar <| a + b calculateTotalPrice : Dollar -> Quantity -> AdditionalFee -> Dollar calculateTotalPrice dollar fee quantity = ( multiply dollar quantity ) |> plus fee calculateTotalPrice ( Dollar 10 ) ( Quantity 10 ) ( Dollar 5 ) -- compilation error!

This results in a compilation error:

Detected errors in 1 module. -- TYPE MISMATCH --------------------------------------------------------------- The argument to function `plus` is causing a mismatch. 19| plus fee ^^^ Function `plus` is expecting the argument to be: Dollar But it is: Quantity ... truncated

Excellent! The compiler is telling us we can’t add a Dollar and a Quantity together, pointing out our error. Let’s make the fix:

calculateTotalPrice : Dollar -> Quantity -> AdditionalFee -> Dollar calculateTotalPrice dollar ( Quantity quantity ) fee = ( multiply dollar quantity ) |> plus fee calculateTotalPrice ( Dollar 10 ) ( Quantity 10 ) ( Dollar 5 ) -- Dollar 105 : Dollar

This compiles successfully, returns the correct value, and is arguably easier to understand:

-- what do these numbers mean!? calculateTotalPrice 10 10 5 -- more obvious what the numbers mean calculateTotalPrice ( Dollar 10 ) ( Quantity 10 ) ( Dollar 5 )

Product types allow for expressive type signatures that correctly convey intent, and the compiler protects us from doing silly things. All is right in the world… or is it?

Introducing product types isn’t without a cost, however. When we started down the path of using product types in our application, we were unwrapping the value, applying the value to a function, and re-wrapping the result. This became incredibly tedious. After thinking about the problem, though, we recognized that we were basically describing Elm’s version of map (check out List.map and Maybe.map for polymorphic versions).

map : ( Int -> Int ) -> Dollar -> Dollar map f ( Dollar i ) = Dollar <| f i map ( * 5 ) ( Dollar 5 ) -- Dollar 25 : Dollar

With map identified, we noticed another pattern: adding and subtracting Dollar values. (+) and (-) both have the signature number -> number -> number , so map2 feels very similar:

map2 : ( Int -> Int -> Int ) -> Dollar -> Dollar -> Dollar map2 f ( Dollar a ) ( Dollar b ) = Dollar <| f a b map2 ( + ) ( Dollar 17 ) ( Dollar 8 ) -- Dollar 25 : Dollar

This allowed for further sugar by defining add and subtract , leveraging map2 .

Within the investment time project, here’s what we ended up with:

module Currency exposing ( .. ) type Currency = Currency Int zero : Currency zero = Currency 0 add : Currency -> Currency -> Currency add = map2 ( + ) subtract : Currency -> Currency -> Currency subtract = map2 ( - ) divideBy : Int -> Currency -> Currency divideBy divisor = map ( flip ( // ) divisor ) fromInt : Int -> Currency fromInt = Currency toInt : Currency -> Int toInt ( Currency int ) = int greaterThan : Currency -> Currency -> Bool greaterThan ( Currency a ) ( Currency b ) = a > b map : ( Int -> Int ) -> Currency -> Currency map f ( Currency a ) = Currency <| f a map2 : ( Int -> Int -> Int ) -> Currency -> Currency -> Currency map2 f ( Currency a ) ( Currency b ) = Currency <| f a b

As you can see, Currency.map and Currency.map2 ended up being cornerstones to a number of the other functions, and that makes a lot of sense! Because of the underlying Int that we’re wrapping, the functions passed are always either Int -> Int or Int -> Int -> Int .

This path came from my liberal use of newtype in Haskell; I enjoy the safety newtype brings and wanted to see how well it translated to Elm.

If you’re familiar with Haskell, you might notice similarities between the signatures of Currency.map / Currency.map2 and mono-traversable ’s omap and ozipWith .

The Haskell equivalent with mono-traversable could be written as:

{-# LANGUAGE TypeFamilies #-} module Currency where import Data.Containers ( MonoZip , ozipWith ) import Data.MonoTraversable ( MonoFunctor , Element , omap ) newtype Currency = Currency Int deriving ( Eq , Ord , Show ) type instance Element Currency = Int instance MonoFunctor Currency where omap f ( Currency i ) = Currency $ f i instance MonoZip Currency where ozipWith f ( Currency i ) ( Currency i' ) = Currency $ f i i' add :: Currency -> Currency -> Currency add = ozipWith ( + ) subtract :: Currency -> Currency -> Currency subtract = ozipWith ( - ) divideBy :: Int -> Currency -> Currency divideBy divisor = omap (` quotInt ` divisor ) where quotInt i i' = fromIntegral i ` quot ` fromIntegral i' zero :: Currency zero = Currency 0 fromInt :: Int -> Currency fromInt = Currency toInt :: Currency -> Int toInt ( Currency i ) = i

Note that greaterThan :: Currency -> Currency -> Bool isn’t necessary because Currency derives the Ord type class, allowing us to use the > operator directly.

One could also derive Num (with GeneralizedNewtypeDeriving ) to leverage (+) , (-) , (*) , and the like:

2 * Currency 5 -- Currency 10 Currency 2 - Currency 1 -- Currency 1

However, it allows for odd interactions:

Currency 2 * Currency 2 -- Currency 4 Currency 5 - 2 -- Currency 3

This feels sloppy, and as such, I favor the explicit functions for the operations I expect to perform on Currency .