There's a lot of type terminology and jargon going around when discussing types in Elm. This glossary attempts to list some of the most common type terms along with synonyms, terms from other language communities, examples, and links to more detailed articles on each topic.
- Custom Type
- Constructor
- Unit Type
- Never Type
- Wrapper Type
- Opaque Type
- Phantom Type
- Recursive Type
- Number
- Comparable
- Record
- Extensible Record
- Type Alias
These are the basic building blocks of data modeling in Elm. This used to be called a union type but the community moved away from that due to confusion with a different type concept of the same name found in some other languages.
People coming to Elm from other language communities may know these as sum types, algebraic data types (ADT), discriminated unions, or tagged unions (yeah these things go by a lot of different names 🤯).
The official guide has a good intro to custom types.
A custom type's constructors are all the variants of that type. These may take zero or more arguments. People coming from other language communities will sometimes use the more precise term data constructor.
-- `Guest`, `Admin`, and `Regular` are all constructors
-- for the `User` type. they take 0, 1, and 2 arguments
-- respectively
type User
= Guest
| Admin Email
| Regular Email (List Permission)
Constructors are sometimes referred to as tags, particularly if they have a single argument. Wrapping a value in a constructor is sometimes referred to as tagging that value.
The official guide has a good intro to custom types.
A type with only a single possible value. This could be a custom type, or a built-in type like the empty tuple and empty record.
-- Custom type
type MyUnit = MyUnit
-- Empty tuple
()
-- Empty record
{}
When needing a type with only a single value, people commonly reach for the
empty tuple ()
, leading people to commonly refer to it as the unit type.
For more counting how many values could exist for a type, see types as sets.
This is a type that is impossible to construct values for. As a result, it has zero possible values. It is used as a type argument to indicate that some scenarios are impossible.
-- `Task.perform`
-- the second argument is a task that cannot fail
perform : (a -> msg) -> Task Never a -> Cmd msg
For more counting how many values could exist for a type, see types as sets.
Charlie Koster also has a more detailed article on Never
.
This is a regular custom type wrapped around some other value, commonly a primitive. It is used to allow the compiler to know that two values represent fundamentally different quantities. This technique is especially common when dealing with units of measure or just generally trying to avoid primitive obsession.
type Dollar = Dollar Int
You'll occasionally see people refer to these as a newtype because it
behaves like what you'd get with Haskell's newtype
keyword.
When compiling with the --optimize
flag, the compiler will "unbox" these
values into raw ints/floats/strings in the JavaScript so using wrapper types has
the same performance characteristics as using primitives directly.
These are types whose constructors are private. This allows the author to control how the type can be instantiated and how the inner data can be accessed.
-- Opaque
module Time exposing (Posix)
type Posix = Posix Int
-- Not opaque
module Time exposing (Posix(..))
type Posix = Posix Int
The official Elm design guidelines recommend package authors use these because it makes it easier to change a package's implementation without breaking changes for users.
Opaque types are often also used to enforce validations.
Note that opaque types combos with many other type techniques so it's possible to have an opaque unit type or an opaque wrapper type.
These are types that have a type variable but don't use it.
-- `a` is unused
type Currency a = Currency Int
They are used to let the compiler know two values represent different quantities while also letting them share common functions. More about phantom types.
It's possible to define types in terms of themselves. For example, a binary tree contains a value, and two sub-trees (who are themselves binary trees).
type BinaryTree a
= Node a (BinaryTree a) (BinaryTree a)
| Empty
The lowercase number
type refers to values that are either floats or integers.
This is mostly helpful so you don't need to define the arithmetic operators
separately for each type.
Note that this type cannot be extended. If you create some custom Rational
type, you can't make it be treated like a number
.
You will sometimes see this referred to as the number
typeclass because it
sort of behaves like a typeclass from Haskell and other languages.
The lowercase comparable
type refers to any values that can be compared with
greater-than or less-than. This includes all the primitives as well as records,
tuples, and lists of primitives. Custom types are not comparable.
As of Elm 0.19, the keys of a Dict
can only be comparable
values.
You will sometimes see this referred to as the comparable
typeclass
because it sort of behaves like a typeclass from Haskell and other languages.
A record is a fixed set of key-value pairs. You can use record types directly in your arguments, they don't need to be defined first.
ageDifference : { name : String, age : Int } -> { name : String, age : Int } -> Int
ageDifference user1 user2 =
abs (user1.age - user2.age)
It's common to alias record types for convenience:
type alias User = { name : String, age : Int }
ageDifference : User -> User -> Int
ageDifference user1 user2 =
abs (user1.age - user2.age)
The official guide has an intro to records.
Extensible records allow us to say that a function only depends on a subset of fields in a record.
ageDifference : { a | age : Int } -> { b | age : Int } -> Int
ageDifference user1 user2 =
abs (user1.age - user2.age)
Any records that have the listed field(s) are permitted values.
-- Given
user = { name = "alice", age = 42 }
building = { height = 1776, age = 5 }
-- We can use arguments of different types
-- because both have an `age` integer field
ageDifference user building
Check out Charlie Koster's guide to extensible records for a more in-depth look.
Type aliases are alternate names for existing types.
-- `Text` and `String` are now interchangeable
type alias Text = String
Type aliases are most commonly used to create short names for record types. As a bonus, when aliasing record types the compiler will generate a constructor function with the same name as the alias.
-- `User` and `{ name : String, age : Int}` are now interchangeable
type alias User =
{ name : String
, age : Int
}
-- We get a free constructor function named `User`
User "Bob" 42
-- Returns `{ name = "Bob", age = 42 }`
- The advanced types in Elm series by Charlie Koster
- The chapter on types from the elm programming book
@Ben52 I'm unsure about including type variables. The goal of this document is to be more of a glossary than a guide. I don't want to just recreate the official guide's explanation. I suppose I could add a short section defining the term and an example. I don't think it would be enough to help readers differentiate between
List a
andList Int
but if I link to some other resources maybe that's good enough? Thoughts?As an aside, I've created a series of short exercises to get a better feel on type variables and polymorphism in Elm.