Elm’s type system is sufficiently sophisticated that you’ll often want to make
fine-grained distinctions between roughly similar types. In a
recent project, for instance, we ended up
with a separate type for a Mother
and a Child
.1 Now, a Mother
is a little different than a Child
. Most obviously, mothers have children,
whereas (at least, in our data model) children do not. So, it was nice for them
to be separate types. In fact, there were certain operations which could be done
on a Mother
but not a Child
(and vice versa). So it was nice to be able to
enforce that at the type level.
Yet, a Mother
and a Child
clearly have a lot in common as well. For this
reason, it sometimes felt natural to write functions that could accept either. So, in
those cases, it was a little awkward for them to be separate types. Something
was needed to express a relationship between the two types.
What alternatives are available to do this sort of thing in Elm? Which did we end up choosing? For answers to these questions, and more, read on!
Our Data Model
Before proceeding further, here’s a little excerpt from our data model, to get us started. Of course, some of this would have been a little different had we chosen a different approach, so we’ll explore that as well. But this will give you a sense of the kind of data we were working with. (I’ve simplified things a bit, compared with the actual app).
type Gender
= Female
| Male
type alias Child =
{ name : String
, avatarUrl : Maybe String
, motherId : MotherId
, birthDate : NominalDate
, gender : Gender
}
type alias Mother =
{ name : String
, avatarUrl : Maybe String
, children : List ChildId
, birthDate : NominalDate
}
type MotherId =
MotherId Int
type ChildId =
ChildId Int
You’ll notice that each Child
has a reference not to a Mother
, but to a
MotherId
. Similarly, each Mother
has a list of ChildId
, not a list of
Child
. The reasons we do things this way are a little subtle – perhaps they
deserve a blog post of their own someday. (Amongst other things, it keeps the
client data model closer to the data model on the backend, which makes JSON
encoding and decoding simpler). However, it does mean that we need some kind of
“container” type in order to get actual children and mothers. In our case, the
relevant parts of that type looked something like this (again, I’ve simplified
a little bit compared to the actual app):
type alias Container =
{ mothers : EveryDict MotherId Mother
, children : EveryDict ChildId Child
, motherMeasurements : EveryDict MotherId MotherMeasurements
, childMeasurements : EveryDict ChildId ChildMeasurements
}
type alias MotherMeasurements =
{ familyPlanningMethod : List FamilyPlanningMethod
}
type alias ChildMeasurements =
{ height : List Height
, weight : List Weight
, photo : List Photo
}
Now, what are all these “measurements”? They were the main point of the app we were working on – we were collecting and displaying those measurements (and a few others I’ve omitted for the sake of simplicity).
Note that these measurements are themselves an example of related types. You
could, for instance, say that ChildMeasurements
is to Child
as
MotherMeasurements
is to Mother
. Furthermore, we needed another couple of
related types to represent the various measurements at the data level (rather
than just the type level)…something like:2
type ChildActivity
= TakePhoto
| MeasureHeight
| MeasureWeight
type MotherActivity
= RecordFamilyPlanningMethod
Now, these types are all reasonably sensible on their own. However, there is clearly some kind of relationship amongst them. You could even imagine giving names to some of those relationships:
Child
andMother
could both be a kind ofPerson
.ChildMeasurements
andMotherMeasurements
could both be a kind ofMeasurement
.ChildActivty
andMotherActivity
could both be a kind ofActivity
.
But how can we express those kind of relationships in Elm’s type system? Or, to put it another way, how might you fill in the gaps in this pseudo-code:
type Person = ???
type Measurement = ???
type Activity = ???
To evaluate the available alternatives, you’ll need to know a little more about the sort of work that we wanted to do with these types – in other words, the functions we wanted to be able to write. In pseudo-code, here are some example function signatures that we ended up needing. (You can probably form an intuition about why we’d want most of these things, and we’ll discuss some of them in more detail below).
getAllActivities : Person -> List Activity
getAvatarUrl : Person -> Maybe String
getBirthDate : Person -> NominalDate
getChildren : PersonId -> Container -> List ChildId
getMotherId : PersonId -> Container -> Maybe MotherId
getName : Person -> String
hasPendingActivity : PersonId -> Activity -> Container -> Bool
iconClass : Person -> String
getMeasurements : PersonId -> Container -> Measurements
So, what approaches are possible? I can think of four:
- Tagged types
- Type classes
- Just don’t do it
- Extensible records
Tagged Types
If what we want is to define a type that can either by a Child
or a
Mother
, the simplest way of doing it is via a tagged type (sometimes
referred to as a sum type, union type, or algebraic data type). That is the
most basic way in which we can define something which might be one sort of
thing or another sort of thing. So, a Person
could be defined in this way:
type Person
= Mother Mother
| Child Child
type PersonId
= PersonIdMother MotherId
| PersonIdChild ChildId
What we have here is a kind of “wrapper” type (Person
), which can be one of
two things (either a Child
or a Mother
). To identify which it is, we re-use
the words Child
and Mother
as “tags” which identify, for a particular
Person
, what sort of person it is.3
Now, this will no doubt be very familiar to you, and it is certainly the first thing you ought to think about when expressing this kind of relationship between types. It is the simplest approach, and often fits the bill admirably. Consider, for instance, how we might implement a few of the functions we sketched above:
getAvatarUrl : Person -> Maybe String
getAvatarUrl person =
case person of
Child child ->
child.avatarUrl
Mother mother ->
mother.avatarUrl
iconClass : Person -> String
iconClass person =
case person of
Child _ ->
"child"
Mother _ ->
"mother"
getMotherId : PersonId -> Container -> Maybe MotherId
getMotherId personId container =
case personId of
PersonIdChild id ->
EveryDict.get id container.children
|> Maybe.map .motherId
PersonIdMother id ->
Just id
getChildren : PersonId -> Container -> List ChildId
getChildren personId container =
getMotherId personId container
|> Maybe.andThen (\id -> EveryDict.get id container.mothers)
|> Maybe.map .children
|> Maybe.withDefault []
That all works very well, so far. However, this approach turned out to have some pitfalls in our app, especially when we started implementing functions which depended on additional related types. Consider how one might express those other types:
type Measurements
= MotherMeasurements MotherMeasurements
| ChildMeasurements ChildMeasurements
type Activity
= MotherActivity MotherActivity
| ChildActivity ChildActivity
So far, so good, and some additional functions can be implemented in a perfectly reasonable way, e.g.:
getAllActivities : Person -> List Activity
getAllActivities person =
case person of
Mother _ ->
List.map MotherActivity
[ RecordFamilyPlanningMethod ]
Child _ ->
List.map ChildActivity
[ TakePhoto
, MeasureHeight
, MeasureWeight
]
However, we start to run into some trouble when implementing functions that
depend on multiple related types. Consider how we might implement
hasPendingActivity
. The original implementation looked something roughly like
this (simplified a little compared to the actual app):
hasPendingActivity : PersonId -> Activity -> Container -> Bool
hasPendingActivity personId activity container =
case ( personId, activity ) of
( PersonIdMother id, MotherActivity motherActivity ) ->
hasPendingMotherActivity id motherActivity container
( PersonIdChild id, ChildActivity childActivity ) ->
hasPendingChildActivity id childActivity container
_ ->
-- Hmm. If we get here, we've got a mis-match between the
-- personId type and the activity type ... one is suitable for
-- a mother, but the other is suitable for a child. So, what
-- answer should we give?
Debug.crash "Mismatched parameters"
Now, whenever you’re writing code and you think to yourself “we shouldn’t get
here”, that’s a clue that you haven’t structured your types quite as well as
you might. Ideally, you should be able to prove to the compiler that you can’t
get there, so you don’t have to write the code at all. Or, you should at least
be able to prove to yourself that you can’t get there, even if it’s hard to
convince the compiler (that’s one reasonable case for Debug.crash
in
production code). However, in this case we can do neither. It’s perfectly
possible to call the function with a mis-match between the PersonId
and the
Activity
, so we have to figure out what answer to give in that case.
You might be thinking that it would be perfectly reasonable to answer False
instead of using Debug.crash
. After all, a Mother
presumably doesn’t have a
pending ChildActivity
. Yet there are a couple of problems with this. Calling
the function with these mis-matched parameters presumably indicates a bug of
some kind in your program logic. It would be better to prevent this bug with
appropriately designed types, rather than acting as though there were some
reasonable way to continue. Furthermore, what happens if you apply a not
to
the result of calling hasPendingActivity
? All of a sudden, it doesn’t seem so
reasonable to have returned False
to a non-sensible question.
Now, I suppose one option to deal with this would be to change the signature
of hasPendingActivity
to return a Maybe
(or a Result
)…something like:
hasPendingActivity : PersonId -> Activity -> Container -> Maybe Bool
That way, we could return Nothing
in cases where there is a mismatch between
our parameters. This would be technically correct at some level, but is really
ugly. Perhaps we can do better!
Type Classes
What if we conceived of the Person
type not as a “wrapper” for Child
and
Mother
, but instead as a set of requirements that we must fulfill in order to
deal with a type as a person? In Elm, we could express those requirements in
the form of a record, along these lines (with some omissions):
type alias Person p =
{ getAllActivities : p -> List Activity
, getAvatarUrl : p -> Maybe String
, getBirthDate : p -> NominalDate
, getName : p -> String
, iconClass : p -> String
}
In effect, we’re saying that any type (here, represented as p
) can be a
Person
, so long as you provide an implementation for each of the indicated
functions. So, we need to fill in the record for Child
and for Mother
,
something like this:
childPerson : Person Child
childPerson =
{ getAllActivities =
always <|
List.map ChildActivity
[ TakePhoto
, MeasureHeight
, MeasureWeight
]
, getAvatarUrl = .avatarUrl
, getBirthDate = .birthDate
, getName = .name
, iconClass = always "child"
}
motherPerson : Person Mother
motherPerson =
{ getAllActivities =
always <|
List.map MotherActivity
[ RecordFamilyPlanningMethod
]
, getAvatarUrl = .avatarUrl
, getBirthDate = .birthDate
, getName = .name
, iconClass = always "mother"
}
So Person
now describes a class of types (or, perhaps, a “typeclass”) with
certain capabilities. If we’re writing a function that needs to make use of one
of those capabilities, we no longer have to specify exactly which type
it takes. Instead, we can ask for any type that has the capability we need.
Consider, for instance, a function to view a person. In our first attempt at
this, with a tagged type for Person
, a simple version might look something
like this.
viewPerson : Person -> Html a
viewPerson person =
div []
[ h4 [] [ text <| getName person ]
, getAvatarUrl person
|> maybeViewImage (iconClass person)
]
Using Person
as a typeclass instead would look something like this:4
viewPerson : Person p -> p -> Html a
viewPerson personConfig person =
div []
[ h4 [] [ text <| personConfig.getName person ]
, personConfig.getAvatarUrl person
|> maybeViewImage (personConfig.iconClass person)
]
This leads to a corresponding difference in how you would call the two
functions. In our previous case, assuming you knew you had a Child
, you’d
need to wrap it up as a Person
…something like viewPerson (Child child)
.
Using typeclasses, you can simply supply the child itself, along with the
implementation which allows you to treat a child as a person…something like
viewPerson childPerson child
.
Now, so far, we haven’t actually gained a lot by using Person
as a typeclass
instead of a tagged type – it’s just an alternate idiom, so far. The
compelling advantage comes when dealing with additional related types. Consider
something like getAllActivities
and hasPendingActivity
. Instead of dealing
with a wrapped Activity
type, they can now deal directly with the exact type
that expresses the activities which are relevant to this kind of person. This
can be expressed in Elm as additional type parameters to the Person
record,
along these lines:
type alias Person p a id m =
{ getAllActivities : p -> List a
, getAvatarUrl : p -> Maybe String
, getBirthDate : p -> NominalDate
, getChildren : id -> Container -> List ChildId
, getMotherId : id -> Container -> Maybe MotherId
, getName : p -> String
, hasPendingActivity : id -> a -> Container -> Bool
, iconClass : p -> String
, getMeasurements : id -> Container -> m
}
You might call this a “multi-parameter type class”, if you were so inclined.
Adding these additional type parameters neatly solves our difficulty with
implementing hasPendingActivity
, since we can now depend on getting an
activity of the appropriate type – the compiler will complain if we don’t. So,
we can do something like:
motherConfig : Person Mother MotherActivity MotherId MotherMeasurements
motherConfig =
{ hasPendingActivity = hasPendingMotherActivity
, ...
}
childConfig : Person Child ChildActivity ChildId ChildMeasurements
childConfig
{ hasPendingActivity = hasPendingChildActivity
, ...
}
hasPendingChildActivity : ChildId -> ChildActivity -> Container -> Bool
hasPendingChildActivity childId childActivity container =
...
hasPendingMotherActivity : MotherId -> MotherActivity -> Container -> Bool
hasPendingMotherActivity motherId motherActivity container =
...
There can no longer be any mismatches between the id
and the activity
supplied to hasPendingActivity
, since the compiler is aware of all the
related types and will complain if any of them are wrong.
So, while you might have heard that Elm lacks typeclasses, my own experience is
that I use them (at least somewhere) in pretty much every app. You can also
find examples of this technique in packages such as
elm-sortable-table,
where the Config data msg
in view : Config data msg -> State -> List data -> Html msg
is a kind of typeclass.
There are a few limitations of typeclasses in Elm which I should
mention. One is that you must explicitly mention the typeclass implementation
when you call a function that requires one. So, for instance, to call the
viewPerson
function sketched above, you need to explicitly refer to the
typeclass…something like viewPerson childConfig child
, or viewPerson motherConfig mother
. In some languages, the compiler can deduce that if
you’re supplying a child
to viewPerson
, you must want to use the
childConfig
, so you don’t have to mention it explicitly. That is a
convenience, but it is often confusing to try to follow the compiler’s
reasoning about which typeclass instance it will deduce. It may not be
such a terrible thing to just say which one you want. Saying which typeclass
you want also makes it easy to construct typeclasses at run-time, which is
occasionally handy, and is more convoluted (but not impossible) in languages
like Haskell.
A bigger difficulty is Elm’s lack of higher-kinded polymorphism. What this
means is that type parameters cannot be used as wrappers – they can only be
wrapped. So, in the example above, we have to be specific about the List
in List p
. The p
can be a parameter, but we have to make a definite choice
about List
– you must always choose concrete “container” types. So, there
are some relationships between types that you won’t be able to model
accurately. However, that still leaves a lot of room for this technique to be
useful.
Finally, there are cases in which you really do want a tagged type. Consider a case where you might want to include both children and mothers in the same list.
You can say
List p
, but that is either a list of all mothers, or all children, not a list that can include both.You could do something fancy with existential types (or rank-n types), but Elm doesn’t have those.
So, if you need a list that can include both mothers and children, you’re pretty
much going to need a tagged type. In our app, it turned out that we didn’t need
that for mothers and children, but there were cases in which we wanted lists
of activities. So, in addition to the Person
record, we also had a tagged type
for activities.
type Activity
= MotherActivity MotherActivity
| ChildActivity ChildActivity
Once you have that, constructing a List Activity
is straightforward. We also
added something to Person
so that we could convert (when necessary) from its
generic activity type to the more concrete Activity
, along these lines:
type alias Person p a id m =
{ ...
, wrapActivity : a -> Activity
}
childConfig : Person Child ChildActivity ChildId ChildMeasurements
childConfig =
{ ...
, wrapActivity = ChildActivity
}
motherConfig : Person Mother MotherActivity MotherId MotherMeasurements
motherConfig =
{ ...
, wrapActivity = MotherActivity
}
This allows you to use both approaches (tagged types and typeclasses) together, in cases where you need both. However, it is often the case that just one or the other approach is fine on its own.
Just Don’t Do It
A question which may have occurred to some readers by now is whether we have
really gained anything by expressing a relationship between Mother
and
Child
(whether by tagged types or typeclasses). What if we didn’t bother?
What if we just let mothers be mothers and children be children, without
bothering with unifying types? After all, when writing Elm code, the right
answer to some of our impulses is “just don’t do it” – especially when the
impulse is towards greater abstraction.
Consider the hasPendingActivity
function I’ve sketched above. No matter how
we structure things, we still need the more concrete hasPendingChildActivity
and hasPendingMotherActivity
anyway. So, what exactly do we gain by also
having the more abstract version at our disposal?
One way of addressing this question is to look at the viewPerson
function
which I sketched out above:
-- With tagged types
viewperson : person -> html a
viewperson person =
div []
[ h4 [] [ text <| getname person ]
, getavatarurl person
|> maybeviewimage (iconclass person)
]
-- With type classes
viewPerson : Person p -> p -> Html a
viewPerson personConfig person =
div []
[ h4 [] [ text <| personConfig.getName person ]
, personConfig.getAvatarUrl person
|> maybeViewImage (personConfig.iconClass person)
]
What if we didn’t have a Person
type at all – just Mother
and Child
. Then,
we’d end up with something like this:
viewMother : Mother -> Html a
viewMother mother =
div []
[ h4 [] [ text mother.name ]
, maybeViewImage "mother" mother.avatarUrl
]
viewChild : Child -> Html a
viewChild child =
div []
[ h4 [] [ text child.name ]
, maybeViewImage "child" child.avatarUrl
]
I suppose it’s true that we’re repeating ourselves a little bit here. However,
if our view
function really were this simple, it would be obviously better to
repeat ourselves rather than engage in the complexity of constructing a type
class. That is, you wouldn’t want to use type classes to avoid this little bit
of repetition – the repetition would be less painful (and, fewer lines of
code) than the type classes.
Of course, in our real app, the view
function was much more complex than this.
It’s always difficulty writing about programming techniques that only make sense
in the presence of complexity, since it would be tedious, and distracting, to
actually provide all the complexity. But, consider the following sort of pseduo-code
that is somewhat typical:
viewMother : Mother -> Html a
viewMother mother =
let
nameBlock =
viewName mother.name
imageBlock =
maybeViewImage "mother" mother.avatarUrl
part3 =
...
part4 =
...
part5 =
...
part6 =
...
in
div []
[ nameBlock
, imageBlock
, part3
, part4
, part5
, part6
]
Now, suppose you want to be able to view a Child
in a roughly similar way,
while avoiding as much code repetition as is reasonable. One way is to
generalize the whole function, using tagged types or type classes (the
approaches we’ve considered so far). But that’s not the first technique I’d
actually reach for. The first technique I’d reach for is generalizing the
parts of the function. So, you could imagine something like this for
viewChild
:
viewChild : Child -> Html a
viewChild child =
let
nameBlock =
viewName child.name
imageBlock =
maybeViewImage "child" child.avatarUrl
part3 =
...
part4 =
...
part5 =
...
part6 =
...
in
div []
[ nameBlock
, imageBlock
, part3
, part4
, part5
, part6
]
Here, we’re identifying multiple “helper” functions, like viewName
and
maybeViewImage
, which we can re-use inside our two distinct functions,
rather than trying to create a single, master function. Now, this does leave
some repetition in viewChild
and viewMother
. However, the guts of the logic
is not repeated, since it is broken out into multiple functions that both
viewChild
and viewMother
can call. Depending on what kind of computation
goes into part3
, part4
, part5
and part6
, we may have many more
opportunities to create additional helper functions.
Why is this often a preferable approach? One reason is that the helper
functions often make a great deal of sense in their own right – you’d probably
want to create them anyway. One of the main questions I ask myself when
structuring code is: how can I structure this in a way that I can be confident
that it’s finished – that I won’t have to revisit it? That is a lot easier to
do for viewNameBlock
than it is for viewMother
. Well, I suppose you might
want to change the way the name is viewed at some point. But it’s a small,
well-defined function that you may be able to just leave alone for quite a
while. That’s the kind of function I like.
The other reason is that the similarities between viewChild
and viewMother
may not be as profound as you initially thought. What if the product manager
comes back to you tomorrow, humble developer, and says that children ought to
be viewed in a subtly different way than mothers? (They are known to do such
things). If you’ve created a viewPerson
function that handles both mothers
and children, you can deal with that by creating some sort of configuration in
your Person
type – for instance, something like the iconClass
I use above.
However, if you have to do too much of that, your viewPerson
function can
become convoluted and tricky to understand. It might have been better to break
out a bunch of helper functions, so that you can make more comprehensible
changes in separate viewChild
and viewMother
functions.
Now, in the case of the app I was working on, I was pretty confident that the
view for children was going to stay structurally similar to the view for
mothers, as we continued to develop the app. So, it seemed worthwhile to
construct a unified viewPerson
function using type classes. But, you can
never be entirely sure – you have to rely on an intuition as to which
techniques best fit which situation. After all, you can’t really sketch out
each approach in detail and see which is actually nicer – it would take too long!
(Unless you’re writing a blog post!). Instead, you need to think about the
app’s requirements now, and what they are likely to become, and make the best
choice you can.
Extensible Records
Finally, I should at least mention one option which wasn’t suitable for our example, but which could be interesting in some cases: extensible records.
When you have two related types that are both “record” types, and they share
some field definitions in common, it is possible to use Elm’s record syntax to
work with just those fields. Consider a getName
function which we want to use
with both Mother
and Child
. Both types have a field defined as name : String
. So,
we could define a getName
function like this:
getName : { a | name : String } -> String
getName thingWithAName =
thingWithAName.name
-- Or, more succintly
getName2 : { a | name : String } -> String
getName2 =
.name
Either of those functions can be called with a Mother
or a Child
, because
both a Mother
and a Child
have the required name
field. (The a
type
parameter tells the compiler that the argument can have other fields as well,
in addition to name
).
Now, as getName2
suggests, you don’t really even
need to define getName
, since Elm will magically build it for you if you say
.name
. But, this can get more interesting where two types share multiple
fields. It becomes possible to define a type that expresses all the fields
that the two types have in common. For instance, you could imagine a Person
type that contains the fields that Child
and Mother
have in common:
type alias Person a =
{ a
| name : String
, avatarUrl : Maybe String
, birthDate : NominalDate
}
Then, you could almost write the simplest version of our viewPerson
function
in a way that will accept either a Child
or a Mother
as an argument.
viewPerson : Person a -> Html b
viewPerson person =
div []
[ h4 [] [ text person.name ]
, maybeViewImage "?" person.avatarUrl
]
This can be a useful technique in simple cases. However, even our simplest
version of viewPerson
isn’t quite simple enough. We want to associate a
different iconClass
with mothers and children, for use in maybeViewIamge
.
Extensible records don’t, by themselves, provide a mechanism for doing that.
So, you’ll often need more complex techniques than extensible records. However,
in cases where extensible records are sufficient, they are very convenient.
Notes
There were no fathers in our app’s data model. ↩︎
You might think it odd that the
MotherMeasurements
andMotherActivty
types have just one alternative (familyPlanningMethod
andRecordFamilyPlanningMethod
respectively). The explanation is that they could eventually have more, so it was nice to start with a structure that would accommodate that. ↩︎Elm can distinguish tags from the types by context, so you can re-use the type names as tag names if you like. This is what we do with
Mother Mother
, where the firstMother
is a tag and the secondMother
is a type. Or, you can invent arbitrary tags. This is necessary forPersonIdMother MotherId
– we can’t just sayMotherId MotherId
because we’ve already usedMotherId
as a tag when we definedtype MotherId = MotherId Int
. At least, we can’t do it in the same module, since the two uses ofMotherId
as a tag would clash. ↩︎If you happen to be familiar with languages like Haskell which have explicit support for typeclasses, the declaration of this kind of function wouldn’t actually look much different. Instead of
viewPerson : Person p -> p -> Html a
, you’d have something likeviewPerson :: Person p => p -> Html a
. ↩︎