chharvey / counterpoint

A robust programming language.
GNU Affero General Public License v3.0
2 stars 0 forks source link

Generics — Type Aliases #66

Open chharvey opened 3 years ago

chharvey commented 3 years ago

Generic Types formalize the concept of variable types — types that are variable and may change.

Discussion

A typical example: A generic type Nullable<T> that takes a single type argument T and produces a union of T and null.

type Nullable<T> = T | null;

T is not a type, but a type variable that acts like a parameter. The act of providing an actual value for T is called specifying the generic type Nullable. It can also be thought of as “calling” or “instantiating” it.

let my_value_int: Nullable.<int> = 42;   % resolves to type `int | null`
let my_value_str: Nullable.<str> = null; % resolves to type `str | null`

Notice the difference in syntax between generic type declaration and generic type specification: Nullable<T> versus Nullable.<int>.

Generic type parameters can be reused across type declarations.

type Nullable<T> = T | null;
type And<T, U>  = T & U;
% parameter `T` is reused

Providing the incorrect number of arguments resolves in a TypeError.

type Or<T, U> = T | U;
let x: Or.<int> = 42;  %> TypeError: Got 1 type arguments, but expected 2.

Generic type aliases can be thought of as “functions” that have “parameters” and are called with “arguments”, however this is not exactly accurate, since type functions are a related but separate feature (#73). Type generics are resolved eagerly, and like non-generics, they cannot be recursive.

type Or<A, B> = A | Or.<B, null>; %> ReferenceError: `Or` is not defined

Optional Parameters

Generic type aliases can be defined with optional generic parameters, which must have a default value. When the generic is specified, the argument may be omitted, in which case the default value is assumed. All optional parameters must come after all required parameters.

type Or<T, U ?= null> = T | U;
type X = Or.<int, bool>;       % resolves to type `int | bool`
type Y = Or.<int>;             % resolves to type `int | null`

Constrained Parmeters

Use the narrows keyword to constrain a generic parameter. When a parameter T narrows U is declared, the argument sent in for T must be a subtype of U, otherwise it’s a type error.

type Nullish<T narrows int | float> = T | null;
type Z = Nullish.<str>;                         %> TypeError: Type `str` is not a subtype of type `int | float`.

Specification

Lexicon

Keyword :::=
    // modifier
+       | "narrows"
;

Syntax

+ParameterGeneric<Optional>
+   ::= IDENTIFIER ("narrows" Type)? <Optional+>("?=" Type);

+ParametersGeneric ::=
+   |  ParameterGeneric<-Optional># ","?
+   | (ParameterGeneric<-Optional># ",")? ParameterGeneric<+Optional># ","?
+;

-DeclarationType ::= "type" IDENTIFIER                                   "=" Type ";";
+DeclarationType ::= "type" IDENTIFIER ("<" ","? ParametersGeneric ">")? "=" Type ";";

Semantics

+SemanticHeritage    ::= SemanticType;
+SemanticDefaultType ::= SemanticType;

+SemanticTypeParam
+   ::= SemanticTypeAlias SemanticHeritage? SemanticDefaultType?;

-SemanticDeclarationType
+SemanticDeclarationTypeAlias
    ::= SemanticTypeAlias SemanticType;

+SemanticDeclarationTypeGeneric
+   ::= SemanticTypeAlias SemanticTypeParam+ SemanticType;

SemanticDeclaration =:=
    | SemanticDeclarationVariable
-   | SemanticDeclarationType
+   | SemanticDeclarationTypeAlias
+   | SemanticDeclarationTypeGeneric
;

Decorate

+Decorate(ParameterGeneric<-Optional> ::= IDENTIFIER) -> SemanticTypeParam
+   := (SemanticTypeParam
+       (SemanticTypeAlias[id=TokenWorth(IDENTIFIER)])
+   );
+Decorate(ParameterGeneric<-Optional> ::= IDENTIFIER "narrows" Type) -> SemanticTypeParam
+   := (SemanticTypeParam
+       (SemanticTypeAlias[id=TokenWorth(IDENTIFIER)])
+       (SemanticHeritage Decorate(Type))
+   );
+Decorate(ParameterGeneric<+Optional> ::= IDENTIFIER "?=" Type) -> SemanticTypeParam
+   := (SemanticTypeParam
+       (SemanticTypeAlias[id=TokenWorth(IDENTIFIER)])
+       (SemanticDefaultType Decorate(Type))
+   );
+Decorate(ParameterGeneric<+Optional> ::= IDENTIFIER "narrows" Type__0 "?=" Type__1) -> SemanticTypeParam
+   := (SemanticTypeParam
+       (SemanticTypeAlias[id=TokenWorth(IDENTIFIER)])
+       (SemanticHeritage Decorate(Type__0))
+       (SemanticDefaultType Decorate(Type__1))
+   );

+Decorate(ParametersGeneric ::= ParameterGeneric<?Optional># ","?) -> Sequence<SemanticTypeParam>
+   := ParseList(ParameterGeneric<?Optional>, SemanticTypeParam);
+Decorate(ParametersGeneric ::= ParameterGeneric<-Optional># "," ParameterGeneric<+Optional># ","?) -> Sequence<SemanticTypeParam>
+   := [
+       ...ParseList(ParameterGeneric<-Optional>),
+       ...ParseList(ParameterGeneric<+Optional>),
+   ];

-Decorate(DeclarationType ::= "type" IDENTIFIER "=" Type ";") -> SemanticDeclarationType
+Decorate(DeclarationType ::= "type" IDENTIFIER "=" Type ";") -> SemanticDeclarationTypeAlias
-   := (SemanticDeclarationType
+   := (SemanticDeclarationTypeAlias
        (SemanticTypeAlias[id=TokenWorth(IDENTIFIER)])
        Decorate(Type)
    );
+Decorate(DeclarationType ::= "type" IDENTIFIER "<" ","? ParametersGeneric ">" "=" Type ";") -> SemanticDeclarationTypeGeneric
+   := (SemanticDeclarationTypeGeneric
+       (SemanticTypeAlias[id=TokenWorth(IDENTIFIER)])
+       ...Decorate(ParametersGeneric)
+       Decorate(Type)
+   );

-Decorate(Declaration ::= DeclarationType) -> SemanticDeclarationType
+Decorate(Declaration ::= DeclarationType) -> SemanticDeclarationTypeAlias | SemanticDeclarationTypeGeneric
    := Decorate(DeclarationType);
chharvey commented 3 years ago

Update: add widens to parameter constraints.

Use the widens keyword to constrain a type parameter in the opposite direction: to declare it as a supertype.

type Nullish<T widens int> = T | null;
let x: Nullish.<42 | 43> = 42;         %> TypeError: Type `int` is not a subtype of type `42 | 43`.

widens is only recommended when narrows would require accessing latter parameters.

type T<A narrows B, B> = A;
%                ^ ReferenceError: `B` is used before it is declared.

To fix this error, we could switch the parameters:

type T<B, A narrows B> = A;
%                   ^ ok

However, if switching is not possible, we can use the widens keyword:

type T<A, B widens A> = A;
%                  ^ ok

Lexicon

Keyword :::=
    // modifier
        | "narrows"
+       | "widens"
;

Syntax

ParameterGeneric<Optional>
-   ::= IDENTIFIER ( "narrows"             Type)? <Optional+>("?=" Type);
+   ::= IDENTIFIER (("narrows" | "widens") Type)? <Optional+>("?=" Type);

Semantics

-SemanticHeritage                        ::= SemanticType;
+SemanticHeritage[dir: NARROWS | WIDENS] ::= SemanticType;
SemanticTypeDefualt                      ::= SemanticType;

Decorate

Decorate(ParameterGeneric<-Optional> ::= IDENTIFIER "narrows" Type) -> SemanticTypeParam
    := (SemanticTypeParam
        (SemanticTypeAlias[id=TokenWorth(IDENTIFIER)])
-       (SemanticHeritage              Decorate(Type))
+       (SemanticHeritage[dir=NARROWS] Decorate(Type))
    );
+Decorate(ParameterGeneric<-Optional> ::= IDENTIFIER "widens" Type) -> SemanticTypeParam
+   := (SemanticTypeParam
+       (SemanticTypeAlias[id=TokenWorth(IDENTIFIER)])
+       (SemanticHeritage[dir=WIDENS] Decorate(Type))
+   );

Decorate(ParameterGeneric<+Optional> ::= IDENTIFIER "narrows" Type__0 "?=" Type__1) -> SemanticTypeParam
    := (SemanticTypeParam
        (SemanticTypeAlias[id=TokenWorth(IDENTIFIER)])
-       (SemanticHeritage              Decorate(Type__0))
+       (SemanticHeritage[dir=NARROWS] Decorate(Type__0))
        (SemanticTypeDefault Decorate(Type__1))
    );
+Decorate(ParameterGeneric<+Optional> ::= IDENTIFIER "widens" Type__0 "?=" Type__1) -> SemanticTypeParam
+   := (SemanticTypeParam
+       (SemanticTypeAlias[id=TokenWorth(IDENTIFIER)])
+       (SemanticHeritage[dir=WIDENS] Decorate(Type__0))
+       (SemanticTypeDefault Decorate(Type__1))
+   );
chharvey commented 5 months ago

The constraints of generic alias “parameters” is now expanded to type aliases themselves.

Optionally, a type alias may be declared with a narrows or widens constraint to further stricten the declaration.

type JustInt narrows Maybe.<int> = int;

This constraint clause is optional, but helps catch errors early on. The following change is valid, but could be prone to more bugs:

-type JustInt = int;
+type JustInt = float;

With the constraint in place, the typer throws a compile-time error in one place — at the type alias’s declaration site — rather than at all its reference sites.

type JustInt narrows Maybe.<int> = float; %> TypeError: `float` is not a subtype of `Maybe.<int>`

Note that type alias constraints do not (and cannot) apply to generic type aliases.

type MyType<T> narrows T | null = SomeComplicatedGeneric.<T> | T; %> SyntaxError

The reason is that there’s just no way, in the general case and at the declaration site, for the compiler to evaluate the assigned type expression before it’s specified with a type argument.

Syntax

-DeclarationType ::= "type" IDENTIFIER (                              "<" ","? ParametersGeneric ">")? "=" Type ";";
+DeclarationType ::= "type" IDENTIFIER (("narrows" | "widens") Type | "<" ","? ParametersGeneric ">")? "=" Type ";";

Semantics

SemanticDeclarationTypeAlias
-   ::= SemanticTypeAlias                   SemanticType;
+   ::= SemanticTypeAlias SemanticHeritage? SemanticType;

Decorate

Decorate(DeclarationType ::= "type" IDENTIFIER "=" Type ";") -> SemanticDeclarationTypeAlias
    := (SemanticDeclarationTypeAlias
        (SemanticTypeAlias[id=TokenWorth(IDENTIFIER)])
        Decorate(Type)
    );
+Decorate(DeclarationType ::= "type" IDENTIFIER "narrows" Type__0 "=" Type__1 ";") -> SemanticDeclarationTypeAlias
+   := (SemanticDeclarationTypeAlias
+       (SemanticTypeAlias[id=TokenWorth(IDENTIFIER)])
+       (SemanticHeritage[dir=NARROWS] Decorate(Type__0))
+       Decorate(Type__1)
+   );
+Decorate(DeclarationType ::= "type" IDENTIFIER "widens" Type__0 "=" Type__1 ";") -> SemanticDeclarationTypeAlias
+   := (SemanticDeclarationTypeAlias
+       (SemanticTypeAlias[id=TokenWorth(IDENTIFIER)])
+       (SemanticHeritage[dir=WIDENS] Decorate(Type__0))
+       Decorate(Type__1)
+   );