Search code examples
typessyntaxelm

How to read type definitions in Elm


How should I read a type definition like the following?

type Config data msg =
  Config
    { toId : data -> String
    , toMsg : State -> msg
    , columns : List (ColumnData data msg)
    , customizations : Customizations data msg
    }

There seems to be a "repeating" element to it: Config is present on the LHS and the RHS.

Another example is this:

type State =
  State String Bool

What are these type declarations communicating?


Solution

  • The left-hand side names the type you will be creating. On the right hand side, you will define the values the type can actually take. There can be just one or however much you want.

    type MyType = ValueA | ValueB | ValueC
    

    Additionally, you can make types whose values contain extra data. That can be done by following the value name (formally called data constructor, or variant in Elm) by another type. You can use any concrete type and have any number of them. The types and arities (numbers of arguments) do not need to be the same between variants.

    type MyType = ValueA String | ValueB Int (List String) | ValueC
    

    These have all been concrete types. When you have a value, it will always have a concrete type. Suppose you want to create your own pair type. You could define

    type PairOfIntAndInt = PairOfIntAndInt Int Int
    type PairOfIntAndString = PairOfIntAndString Int String
    type PairOfStringAndString = PairOfStringAndString String String
    …
    

    but that would not be very convenient. For that reason, Elm allows you to have type constructors (that is the formal name of the type name on the LHS) with parameters. They will be written in lowercase letters:

    type Pair first second = Pair first second
    

    Many useful core features like Maybe or Result in fact are types that accept parameters. They are also called abstract types. To make them a concrete type, you need to pass them a concrete type [^1] to each parameter.

    Now, let’s look at your code. You have a type constructor Config that takes two parameters.

    type Config data msg =
    

    And the type accepts values created with its one data constructor

      Config
    

    which stores a single value. The type of the value is actually a composed type, a record. It expects four fields and their types will depend on the parameters of the type constructor.

        { toId : data -> String
        , toMsg : State -> msg
        , columns : List (ColumnData data msg)
        , customizations : Customizations data msg
        }
    

    For example, if you have a concrete type Config String Int, it will expect the following value:

    Config
      { toId = someToIdValue
      , toMsg = someToMsgValue
      , columns = someColumnsValue
      , customizations = someCustomizationsValue
    }
    

    where someToIdValue will have to be a function taking String and returning String, toMsg a function from State to Int, and so on.

    For more information, see https://guide.elm-lang.org/types/custom_types.html

    [^1]: Some languages like Haskell actually allow wilder types. https://wiki.haskell.org/Kind