Skip to content

Instantly share code, notes, and snippets.

@JoelQ
Last active February 14, 2024 14:29
Show Gist options
  • Star 59 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • 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

@turboMaCk
Copy link

I would argue that recursive type (like List a) are more common than phantom type.

@razzeee
Copy link

razzeee commented Aug 15, 2019

maybe it makes sense to move this over to https://orasund.gitbook.io/elm-cookbook/ ?

@JoelQ
Copy link
Author

JoelQ commented Aug 15, 2019

I would argue that recursive type (like List a) are more common than phantom type.

@turboMaCk I've added a section on recursive types now 🙂

@JoelQ
Copy link
Author

JoelQ commented Aug 15, 2019

maybe it makes sense to move this over to https://orasund.gitbook.io/elm-cookbook/ ?

@razzeee I think the two projects have different visions. The cookbook is a resource for "How to do X" while this has a much narrower scope of defining type-related concepts and terms from the Elm community.

@andys8
Copy link

andys8 commented Aug 15, 2019

Super helpful. I especially like the references to names in other languages.

@razzeee
Copy link

razzeee commented Aug 15, 2019

maybe it makes sense to move this over to orasund.gitbook.io/elm-cookbook ?

@razzeee I think the two projects have different visions. The cookbook is a resource for "How to do X" while this has a much narrower scope of defining type-related concepts and terms from the Elm community.

You're probably right. Still I hoped for a newbie friendlier place to collect all things helpful for elm. And reading the cookbooks introduction, it doesn't seem to be as clearly focused.

In the Elm Town Podcast 45: It Started off As a Morning Book Jeremy Fairbank, the author of the Book Programming Elm, noticed that there is no Elm cookbook, that would serve as a follow-up to a beginners book. This book aims to fill this empty spot.
This Book is a collection of helpful answers from the Elm subreddit, the Discourse forum, the Elm Slack as well as useful articles from Medium. It will cover things that are missing in the official guide and the Beginning Elm Book by Pawan Poudel.

This would at least fall under the bold sections.

I'm also not sure how well findable a gist will be in a few years, but I might be wrong.

@Ben52
Copy link

Ben52 commented Aug 16, 2019

Great list. I would add type variables, and how they differ from types. This is one of the most common things that trip up newcomers. ie ‘List Int’ vs ‘List a’

@JoelQ
Copy link
Author

JoelQ commented Aug 16, 2019

@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 and List 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.

@turboMaCk
Copy link

I think the discussion about type variables is actually more like a discussion about kinds than about types. Article on wikipedia is quite concise though https://en.wikipedia.org/wiki/Kind_(type_theory)

@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