Nominal types are new in Erlang/OTP 28: https://www.erlang.org/eeps/eep-0069
Previously the dialyzer considered two types to be the same only if they were structurally equal. For example {number(), number()}
created in one place is equivalent to any other {number(), number()}
with structural typing. With nominal typing two types are considered the same only if their type has the same name. In Erlang this means that you have functions with spec
s that say they return a type defined with -nominal Type :: Definition.
.
Nominal types have slightly worse ergonomics since you need to have APIs for producing and modifying the type. For example even though nom_index
is a non_neg_integer()
, we can't say Index + 1
in nom.erl
. Instead we would need a function nom_index:next/1
which returns a nom_index:t()
.
Nominal typing is very powerful, however, and a really great addition to the Erlang type system, because the dialyzer can now distinguish between two types which are structurally equivalent but semantically different. Above we have index and term types like from Raft. They're semantically two different things and you shouldn't pass an index to a function expecting a term or vice versa. Now the dialyzer can catch the mistake on nom.erl:9
where the arguments are swapped:
src/nom.erl
Line 5 Column 1: Function hello/0 has no local return
Line 9 Column 25: The call nom:hello_impl(Term::(nom_term:t() :: 0), Index::(nom_index:t() :: 0)) breaks the contract (nom_index:t(), nom_term:t()) -> {nom_index:t(),nom_term:t()}