Kind
Everything is typed in Erg. Types themselves are no exception. kind represents the "type of type". For example, Int
belongs to Type
, just as 1
belongs to Int
. Type
is the simplest kind, the atomic kind. In type-theoretic notation, Type
corresponds to *
.
In the concept of kind, what is practically important is one or more kinds (multinomial kind). One-term kind, for example Option
, belongs to it. A unary kind is represented as Type -> Type
1. A container such as Array
or Option
is specifically a polynomial kind that takes a type as an argument.
As the notation Type -> Type
indicates, Option
is actually a function that receives a type T
and returns a type Option T
. However, since this function is not a function in the usual sense, it is usually called the unary kind.
Note that ->
itself, which is an anonymous function operator, can also be seen as a kind when it receives a type and returns a type.
Also note that a kind that is not an atomic kind is not a type. Just as -1
is a number but -
is not, Option Int
is a type but Option
is not. Option
etc. are sometimes called type constructors.
assert not Option in Type
assert Option in Type -> Type
So code like the following will result in an error:
In Erg, methods can only be defined in atomic kinds, and the name self
cannot be used anywhere other than the first argument of a method.
# K is an unary kind
K: Type -> Type
K T = Class ...
K.
foo x = ... # OK, this is like a so-called static method
bar self, x = ... # TypeError: cannot define a method to a non-type object
K(T).
baz self, x = ... # OK
Examples of binary or higher kinds are {T: U}
(: (Type, Type) -> Type
), (T, U, V)
(: (Type, Type, Type) - > Type
), ... and so on.
There is also a zero-term kind () -> Type
. This is sometimes equated with an atomic kind in type theory, but is distinguished in Erg. An example is Class
.
Nil = Class()
Containment of kind
There is also a partial type relation, or rather a partial kind relation, between multinomial kinds.
K T = ...
L = Inherit K
L<: K
That is, for any T
, if L T <: K T
, then L <: K
, and vice versa.
∀T. L T <: K T <=> L <: K
higher kind
There is also a higher-order kind. This is a kind of the same concept as a higher-order function, a kind that receives a kind itself. (Type -> Type) -> Type
is a higher kind. Let's define an object that belongs to a higher kind.
IntContainerOf K: Type -> Type = K Int
assert IntContainerOf Option == Option Int
assert IntContainerOf Result == Result Int
assert IntContainerOf in (Type -> Type) -> Type
The bound variables of a polynomial kind are usually denoted as K, L, ..., where K is K for Kind.
set kind
In type theory, there is the concept of a record. This is almost the same as the Erg record 2.
# This is a record, and it corresponds to what is called a record in type theory
{x = 1; y = 2}
When all record values were types, it was a kind of type called a record type.
assert {x = 1; y = 2} in {x = Int; y = Int}
A record type types a record. A good guesser might have thought that there should be a "record kind" to type the record type. Actually it exists.
log Typeof {x = Int; y = Int} # {{x = Int; y = Int}}
A type like {{x = Int; y = Int}}
is a record kind. This is not a special notation. It is simply an enumeration type that has only {x = Int; y = Int}
as an element.
Point = {x = Int; y = Int}
Pointy = {Point}
An important property of record kind is that if T: |T|
and U <: T
then U: |T|
.
This is also evident from the fact that enums are actually syntactic sugar for refinement types.
# {c} == {X: T | X == c} for normal objects, but
# Equality may not be defined for types, so |T| == {X | X <: T}
{Point} == {P | P <: Point}
U <: T
in type constraints is actually syntactic sugar for U: |T|
.
A kind that is a set of such types is commonly called a set kind. Setkind also appears in the Iterator pattern.
Iterable T = Trait {
.Iterator = {Iterator}
.iter = (self: Self) -> Self.Iterator T
}
Type inference for polynomial kinds
Container K: Type -> Type, T: Type = Patch K(T, T)
Container (K).
f self = ...
Option T: Type = Patch T or NoneType
Option(T).
f self = ...
Fn T: Type = Patch T -> T
Fn(T).
f self = ...
Fn2 T, U: Type = Patch T -> U
Fn2(T, U).
f self = ...
(Int -> Int).f() # which one is selected?
In the example above, which patch would the method f
choose?
Naively, Fn T
seems to be chosen, but Fn2 T, U
is also possible, Option T
includes T
as it is, so any type is applicable, Container K , T
also matches `->`(Int, Int)
, i.e. Container(`->`, Int)
as ```Int -> Int`. So all four patches above are possible options.
In this case, patches are selected according to the following priority criteria.
- Any
K(T)
(e.g.T or NoneType
) preferentially matchesType -> Type
overType
. - Any
K(T, U)
(e.g.T -> U
) matches(Type, Type) -> Type
preferentially overType
. *Similar criteria apply for kind of 3 or more. - The one that requires fewer type variables to replace is chosen. For example,
Int -> Int
isT -> T
rather thanK(T, T)
(replacement type variables: K, T) orT -> U
(replacement type variables: T, U). `(replacement type variable: T) is matched preferentially. - If the number of replacements is also the same, an error is given as being unselectable.
1 In type theory notation, *=>*
↩
2 There are subtle differences such as visibility. ↩