Skip to content

Instantly share code, notes, and snippets.

@JoelQ
Last active June 26, 2024 03:05
Show Gist options
  • Save JoelQ/6b303d9ad450537163b6f8f6cf8a4ed8 to your computer and use it in GitHub Desktop.
Save JoelQ/6b303d9ad450537163b6f8f6cf8a4ed8 to your computer and use it in GitHub Desktop.
Elm Type Glossary

Elm Types Glossary

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

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.

Constructor

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.

Unit Type

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.

Never

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.

Wrapper Type

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.

Opaque Type

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.

Phantom 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.

Recursive Type

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

Number

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.

Comparable

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.

Records

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

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 Alias

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 }`

Extra Resources

@eimfach
Copy link

eimfach commented Sep 17, 2019

How can records be a comparable ? The elm compiler says:

I cannot do a comparison with this value:

8|     {cell = "1"} < {cell = "1"}
       ^^^^^^^^^^^^
The left side of (<) is a record of type:

    { cell : String }

But (<) only works on Int, Float, Char, and String values. It can work on lists
and tuples of comparable values as well, but it is usually better to find a
different path.

Hint: Only ints, floats, chars, strings, lists, and tuples are comparable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment