Motivation: A List of Different Things
(This introduction will be too easy for most of you. Sorry for this. We’ll come back to this example after looking at more interesting code.)
On the “#beginners” channel of the Elm Slack, people often ask questions like:
How can I create a list of different things, for example a list with strings and integers?
In other languages it isn’t a problem to define a type like string|number
(TypeScript) and create a list of it, but not so in Elm. The obvious answer for Elm is:
You have to wrap them in a custom type.
For example like in
type Thing
= AString String
| AnInt Int
Now we can create a list of Thing
s:
myListOfThings : List Thing
myListOfThings =
[ AString "one", AnInt 1, AnInt 12 ]
Fine. But why might one want to create a list of different things? What can I do with such a list?
One possible reason could be:
To be able to perform the same actions on the elements, independent of their underlying type.
Let’s say we want to get the “sizes” of the Thing
s in our list. If we had a function like
thingSize : Thing -> Int
we could simply use List.map
:
List.map thingSize myListOfThings
Such a function is easy to implement: we just use a case
and then handle each type in its own branch:
thingSize : Thing -> Int
thingSize thing =
case thing of
AString string ->
stringSize string
AnInt int ->
intSize int
For the “size” of a String
we choose its length:
stringSize : String -> Int
stringSize string =
String.length string
For an Integer, we choose the number of bits it takes to represent the number:
intSize : Int -> Int
intSize int =
if int < 2 then
1
else
1 + floor (logBase 2 (toFloat int))
Now we can get the sizes of the things in our list:
List.map thingSize myListOfThings
--> [ 3, 1, 4 ]
What about modifying a wrapped type? Say we want to “double” our Thing
s:
thingDouble : Thing -> Thing
Again, we use a case
and call the type-specific functions, but in this case we have to post-process the type-specific results with the appropriate wrapping constructor:
thingDouble : Thing -> Thing
thingDouble thing =
case thing of
AString string ->
stringDouble string |> AString
AnInt int ->
intDouble int |> AnInt
“Doubling” a string and an int:
stringDouble : String -> String
stringDouble string =
string ++ string
intDouble : Int -> Int
intDouble int =
2 * int
Now we can double the list elements all at once and get their new sizes:
myListOfThings
|> List.map thingDouble
|> List.map thingSize
--> [ 6, 2, 5 ]
So far, so good. What if we want to support Bool
s too?
We have to add another subtype to the wrapper type:
type Thing
= AString String
| AnInt Int
| ABool Bool
Now we can add wrapped Bool
s to the list:
myListOfThings : List Thing
myListOfThings =
[ AString "one", AnInt 1, AnInt 12, ABool True ]
We also need a size
and a double
function for Bool
s. Here are two contrived implementations just to see whether something is happening:
boolSize : Bool -> Int
boolSize bool =
if bool then
1
else
0
boolDouble : Bool -> Bool
boolDouble bool =
not bool
And we need to use the Bool
functions in the wrapper functions:
thingSize : Thing -> Int
thingSize thing =
case thing of
AString string ->
stringSize string
AnInt int ->
intSize int
ABool bool ->
boolSize bool
thingDouble : Thing -> Thing
thingDouble thing =
case thing of
AString string ->
stringDouble string |> AString
AnInt int ->
intDouble int |> AnInt
ABool bool ->
boolDouble bool |> ABool
Now we can get the sizes of a list containing Bool
s as well:
List.map thingSize myListOfThings
--> [ 3, 1, 4, 1 ]
And we can “double” them and get the new sizes, too:
myListOfThings
|> List.map thingDouble
|> List.map thingSize
--> [ 6, 2, 5, 0 ]
So, to recap: what do we have to do to add yet another wrapped type, for example Char
?
- Implement the type-specific functions (
charSize
,charDouble
) - Add the new subtype to the wrapper type
- Add a new case branch to the wrapper functions (
thingSize
,thingDouble
)
There’s no way in Elm to get around task #1. We cannot automatically “derive” size
and double
functions for a new type. But what if we could omit tasks #2 and #3? Sounds magical?
It is, and it needs magic, the magic of Jeremy’s Interface
module!
Stay tuned…