An intro to constructors in Elm
Some of your types come with Free Functions
Consider a typical type definition of aMsg
type in elm:
type Msg
= UserInput String
| LoggedIn User
| Guest User
| ClickedLogoff
In this definition, the types are
Msg
: this is the type that is being defined.String
: this is the (built-in) type we are using ‘inside’ theMsg
type.User
: some custom type
What about UserInput
, LoggedIn
, Guest
and ClickedLogoff
?
They are functions called constructors. You get them for free. If you define the type, you get the constructors as well.
Constructor functions
UserInput
is a function with a capitalized name. It has the signature String -> Msg
. Which means it takes a String
and outputs a Msg
.
And it’s called a constructor. Because it constructs a Msg
type. Insomething = UserInput value
, the UserInput value
part constructs a Msg
. So the variable something
will be of the type Msg
and will hold the resulting value of UserInput value
.
LoggedIn
is also a function, which takes a User
as its argument. It outputs a Msg
too. And Guest
is a function too, with the exact same argument User
, and the same signature.
ClickedLogoff
is also a constructor, of an equally useful nature (albeit somewhat boring). It takes no arguments, and simply returns a Msg
.
You typically call a function like UserInput
in your view. You pass it to the onInput
handler, when you render the input field:
input [ type_ "text", onInput UserInput ] ...
The onInput
handler is from the Html.Events library. It has the following type signature.
onInput : (String -> msg) -> Attribute msg
This function takes one argument, a function. It wants you to provide a function that turns a String
into a msg
. The msg
in the signature is in lowercase. The onInput
function does not care what type the msg
is. That is up to you. It will simply include that type in the output of the function, in the Attribute msg
. (Attribute msg
is the type for attributes of HTML elements. In the Html.Attribute package there is some explanation and examples)
The UserInput
constructor defined earlier matches this signature and does exactly what’s needed here: it takes a String
(which is whatever the user typed in the input field), and turns it into a Msg
.
Constructors in pattern matching
You can also use these constructors to access the contents of a variable later on. Using a case .. of
statement we can
- Find out which constructor was used.
- Access the contents “behind” that constructor (if there is any).
In the example, both the LoggedIn
constructor and the Guest
constructor carry a User
. With a case statement, we can find out which constructor was used, and access the user information:
case msg of
LoggedIn normalUser ->
.. handle the info from the variable normalUser...
Guest guestUser ->
.. handle info from guestUser ..
Records come with constructors too!
Consider this record definition in Elm:
type alias User =
{ name : String
, age : Int
}
By naming this record User
, you also get a constructor for free. The constructor is also called User
, and it has the signature (in this example) String -> Int -> User
. This function allows you to do User "Bill" 42
to create a new user record.
At first, it may seem weird — maybe even conflicting — that the type and its constructor have the same name. But they will never show up in the same place in code:
- In type definitions of other types, it is always the type, never the constructor. In the
Msg
type definition, theUser
that shows up (twice) is the type. - When
User
shows up in code in type signatures likesetName : String -> User -> User
, it is always the type, never the constructor. - In all other places in your code, if you encounter a
User
, it will be the constructor. So for example, in the linenewUser = User “” 0
, theUser
there is the constructor.
Type alias or type?
When you make a type for the user record from the example above, you can actually do this in two ways:
-- Option 1:
type alias User =
{ name : String
, age : Int
}
-- Option 2 (called "opaque", or "strong typed")
type User =
User
{ name : String
, age : Int
}
In option 2 you make the constructor function explicit.
I could have given the constructor any name, but if you have only 1 constructor, it is common to give it the same name as the type.
The compiler is much more forgiving on type aliases. You are basically saying that User
is just an alias for anything that is a record with a String
called name
and an Int
called age
. So if you have some function defined as:
hasLegalAge : User -> Bool
hasLegalAge user =
...
It is fine if you call it like this:
canBuyOurStuff =
hasLegalAge { name = "", age = 28 }
With a strong typed User
, the compiler will not let you do that. It will complain that you passed some record to hasLegalAge
, but it expected a real (strong) User
type.
The real User
type can be made by calling the constructor function, which is also called User
, so if you change the code to this, it will compile again:
canBuyOurStuff =
hasLegalAge (User { name = "", age = 28 })
The second strong typed code looks much more complicated, so what’s the use of strong types?
For one, it makes it easier to extend the User type. If at some point you want to deal with anonymous users, you can add another constructor to your type:
type User
= User
{ name : String
, age : Int
}
| Anonymous
And the canBuyOurStuff
function from above will still work (!). So less refactoring if you extend your strong type with more constructors.
Another advantage to strong types: If you are making a library, you can make more changes without breaking other code. This happens if you do not make the User
constructor available to the rest of the world. If your module definition is like this
module User exposing (User, newUser, setName, ...)
You would expose the User
type, and some custom (lowercase) functions to create or modify the User
. This gives you the freedom to make changes to the inner workings of the User
type, without breaking the existing functions you exposed. You could add fields for e.g address, hobbies, pets, friends etcetera to the record (and expose more functions to read and write them). And you can do this without breaking the original functions, so that anyone or any code that imports the original functions can still use them without refactoring.
Further reading
If you want to know more about types (or if my post was still not clear), here are some other good resources:
- The chapter on types in the official guide is probably the best place to start.
- Understanding the Elm type system goes deeper into types and how to use them in functions.
- Elm Data Structures has more on type alias vs type for record.
- Elm’s Universal Pattern has more on common and very useful functions (like map) you may need for your types too.
- Elm Design Guidelines, with the official explanation on keeping tags and types hidden in libraries.
- Not for the faint-hearted: An explanation on recursive types and type aliases.
The first time I got really confused about types and constructors with the same name, was when I got started with Json decoding. Which is a mind-bender in itself. The example with the function map2 caused a short-circuit in my brain: “Whut? What is this type doing in a function?” It took me a while to figure out that this was not the “type” that was being referenced, but the constructor for the type alias I defined. [AHA moment]
Hopefully this post will save you from a similar short-circuit. Happy coding!