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?
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