Documentation / Generics

Generics

Generics let one declaration describe a family of concrete types or functions.

Moth keeps the solving local. Type arguments come from the declaration, the call in front of you and its immediate receiving type.

Choose the explanation level: Generic declarations

Generic declarations

A generic declaration describes a family of types or functions using a placeholder type.

A generic struct


    Box type A = |
        value A,
    |
    

A stands for some concrete type. Later you can make a Box of Int, a Box of String, and so on.

A generic function


    identity type A |value A| -> A:
        return value
    ;
    

identity returns whatever type it receives.

The placeholder is scoped

The parameter name is meaningful only inside that declaration. You cannot use A as a value or refer to it from another declaration.

Generics form declaration-site type abstractions for top-level structs, choices and free functions. A generic parameter acts as a compile-time placeholder that stands for a concrete type selected later.

Syntax


    identity type A |value A| -> A:
        return value
    ;

    Box type A = |
        value A,
    |

    Maybe type A ::
        Some | value A |,
        Empty,
    ;
    

Generic parameters follow the declaration name after type. Multiple parameters are separated by commas.

Exact contract

  • Generic parameters are introduced with type after the declaration name.
  • Supported declaration owners are top-level structs, choices and free functions.
  • Parameter names use type-name casing.
  • Parameters are scoped to the declaration.
  • Parameters are compile-time placeholders, not runtime values.
  • Each parameter name must be unique within the declaration.
  • Parameters cannot collide with visible concrete types, type aliases, external package types, builtins or other type-position names.
  • A generic body may use active parameters in local type annotations.
  • Inline parameter sugar such as |value type A| is rejected.
  • Generic parameters do not create type values.
  • Generic declarations are resolved before HIR.

Multiple parameters


    Pair type A, B = |
        first A,
        second B,
    |
    

Unused parameters

A generic parameter must be used in the public type shape of the declaration. An unused parameter is rejected.


    -- Error: parameter 'A' is declared but never used
    identity type A |value String| -> String:
        return value
    ;
    

No inline parameter sugar

Generic parameters belong in the declaration header, not inside ordinary parameter lists.


    -- Error
    identity |value type A| -> A:
        return value
    ;
    

Compiler stage note

Header parsing records generic parameter lists and declaration metadata. AST resolves generic signatures to canonical type identities, validates generic bodies against unconstrained parameters and emits concrete instances before HIR generation. HIR never carries unresolved generic calls.

Related concepts

Choose the explanation level: Type application

Type application

Use of to choose the concrete type for a generic declaration.

One argument


    Box type A = |
        value A,
    |

    text_box Box of String = Box("hello")
    

Several arguments


    Pair type A, B = |
        first A,
        second B,
    |

    item Pair of String, Int = Pair("count", 3)
    

Concrete aliases

When a type would need nested of applications, name an intermediate alias.


    StringIntPair as Pair of String, Int
    value Box of StringIntPair = Box(Pair("count", 3))
    

Concrete generic instances are created with of in type position.

Syntax


    number_box Box of Int = Box(42)
    text_box Box of String = Box("hello")

    Pair type A, B = |
        first A,
        second B,
    |

    item Pair of String, Int = Pair("count", 3)
    

Exact contract

  • of applies concrete type arguments in type position.
  • Argument count must match the generic declaration.
  • Each argument must be a valid concrete type.
  • Different concrete argument lists produce different semantic types.
  • Constructors use ordinary constructor syntax.
  • Explicit generic free-function call type arguments are not supported.

Nested of boundary

Nested inline of applications are rejected. Use a named concrete alias for any nested shape the current compiler rejects.


    -- Error
    value Box of Pair of String, Int = Box(Pair("count", 3))
    

Work around the limit with an intermediate alias:


    StringIntPair as Pair of String, Int
    value Box of StringIntPair = Box(Pair("count", 3))
    

The collection element exception allows one inline of application inside a collection element annotation, such as {Box of String}. Nested of applications inside a collection element are rejected for the same reason as nested of in any other type position: use a named concrete alias.

Concrete generic aliases

A type alias may name a fully applied generic instance. The alias remains transparent.


    StringBox as Box of String

    box StringBox = Box("docs")
    raw Box of String = box
    

Constructed forms are compiler-owned

Public Option of T, Result of T, E, Option.Some and Result.Ok are not language syntax. Options, fallible carriers and collections are compiler-owned constructed forms with their own source syntax.

Related concepts

Choose the explanation level: Generic inference

Generic inference

You call a generic function like any other function. The compiler figures out the concrete type from the immediate context.

Inference from an argument


    identity type A |value A| -> A:
        return value
    ;

    value = identity(42)
    

Because 42 is an Int, A becomes Int for this call.

Inference from the result type


    empty type A || -> {A}:
        return {}
    ;

    items {Int} = empty()
    

The left-hand side says the result is {Int}, so A becomes Int.

Generic function calls use ordinary call syntax. Type arguments are inferred from local evidence only.

Inference sources


    identity type A |value A| -> A:
        return value
    ;

    empty type A || -> {A}:
        return {}
    ;

    value = identity(42)
    typed_value Int = identity(42)
    items {Int} = empty()
    

The concrete type for A can come from:

  • immediate call arguments
  • the immediate expected result type at a closed receiving site

What inference does not use


    -- Error: T is ambiguous at the declaration site
    values = empty()
    

Inference does not use:

  • later mutation
  • later reads
  • whole-program analysis
  • HIR
  • borrow validation
  • distant return use
  • expected parameter context from an outer call into a nested generic call

Use an intermediate annotation when a nested generic call would otherwise need evidence from its parent call.


    empty type A || -> {A}:
        return {}
    ;

    consume |values {Int}|:
    ;

    -- Rejected: outer parameter context does not solve the nested call.
    consume(empty())

    -- Valid: annotate the nested result first.
    items {Int} = empty()
    consume(items)
    

Optional and fallible generic returns

Generic functions may return optional or fallible values when the concrete envelope is solved by immediate evidence.


    wrap_optional type T |value T| -> T?:
        return value
    ;

    always_fail type T, E |fallback T, err E| -> T, E!:
        return! err
    ;

    maybe_name String? = wrap_optional("Ana")

    value = always_fail(7, Error("bad")) catch |err| then 0
    

A bare none cannot infer an otherwise unknown parameter by itself.

Imported generic functions

Visible imported generic functions use the same local inference rules. There is no explicit call-site type argument syntax.

Cast boundary

Generic inference does not look through cast. The cast target must be selected after the generic call has already been solved.

Generic error parameter inference

A concrete callee error type does not specialise an otherwise unsolved generic error parameter merely because it appears inside the generic body. Immediate declaration and call-site evidence must still solve the parameter.


    -- Rejected: E is not solved by the concrete Error inside the body.
    generic_fail type E || -> Int, E!:
        return! Error("bad")
    ;
    

A generic error parameter must be solved by immediate call-site evidence, like any other generic parameter.

Explicit call-site syntax is rejected

All of these are rejected:


    identity of Int(42)
    identity<Int>(42)
    identity[Int](42)
    identity(42 Int)
    

Related concepts

Choose the explanation level: Generic instances

Generic instances

When you apply concrete types to a generic declaration, you get a concrete instance.

A concrete generic struct


    Box type A = |
        value A,
    |

    int_box Box of Int = Box(42)
    string_box Box of String = Box("hello")
    

Box of Int and Box of String are different types. You cannot assign one to the other.

A concrete generic choice


    Maybe type A ::
        Some | value A |,
        Empty,
    ;

    name Maybe of String = Maybe::Some("Ana")
    

Maybe of String stores a String payload. Maybe of Int would store an Int.

A concrete generic instance substitutes the generic parameters with concrete type arguments. The resulting type is ordinary: it has fields, constructors and the same access rules as any other type.

Field and payload substitution


    Box type A = |
        value A,
    |

    int_box Box of Int = Box(42)
    string_box Box of String = Box("hello")
    

Box of Int and Box of String are different semantic types. The field value has type Int in the first and String in the second.

Generic choices


    Maybe type A ::
        Some | value A |,
        Empty,
    ;

    name Maybe of String = Maybe::Some("Ana")
    missing Maybe of String = Maybe::Empty
    

Choice payload matching uses the substituted payload types.

Instance identity

Same constructor plus same concrete arguments yields the same instance type. Different arguments yield incompatible instance types.


    Box type A = |
        value A,
    |

    a Box of Int = Box(1)
    b Box of Int = Box(2)
    -- c Box of String = a -- would be an error
    

Constructor inference

Generic struct and choice constructors infer their concrete type arguments from immediate constructor arguments and from the immediate receiving type. No explicit constructor type arguments are needed or supported.


    Box type A = |
        value A,
    |

    -- Inferred from the constructor argument.
    value = Box(42)

    -- Inferred from the receiving type.
    text_box Box of String = Box("hello")
    

Aligned receiver methods

A receiver method may be declared on a generic type when the method's generic parameters align exactly with the receiver constructor's parameters.


    Box type A = |
        value A,
    |

    get type A |this Box of A| -> A:
        return this.value
    ;

    box Box of Int = Box(42)
    value = box.get()
    

Receiver methods on one concrete generic instance remain rejected.

Generic bounds and evidence

Concrete instantiation with trait bounds requires visible reusable evidence. See Generic trait bounds.

Deferred and rejected surfaces

  • Recursive generic nominal types are rejected.
  • Receiver methods on concrete instances are rejected.
  • Generic external package functions and types are deferred.

Related concepts

Choose the explanation level: Generic limits

Generic limits

Generic code can move an unknown type around, but it needs a bound before calling type-specific behaviour.

Moving values around


    identity type A |value A| -> A:
        return value
    ;
    

identity does not need to know anything about A. It returns what it got.

Needing a bound


    DISPLAY_TEXT must:
        display |This| -> String
    ;

    render type Item is DISPLAY_TEXT |item Item| -> String:
        return item.display()
    ;
    

render can call .display() because the bound promises that Item provides it.

Unconstrained generic code can move values around but cannot assume type-specific behaviour.

What unconstrained generic code may do

  • pass values through
  • return them
  • store them in generic structs or choices
  • forward them to another generic function when immediate evidence solves the call
  • use generic parameters in local annotations

What unconstrained generic code cannot assume

  • arithmetic
  • equality or ordering
  • field access
  • string-like template interpolation
  • external or IO behaviour
  • arbitrary receiver methods

A trait bound may enable the unique receiver behaviour provided by its static contract.


    DISPLAY_TEXT must:
        display |This| -> String
    ;

    render type Item is DISPLAY_TEXT |item Item| -> String:
        return item.display()
    ;
    

Rejected or deferred current Alpha surfaces

  • explicit call-site generic syntax
  • inline generic parameter sugar such as |value type A|
  • recursive generic types
  • receiver methods on concrete instances
  • generic external package functions and types
  • nested inline of applications beyond the accepted single application
  • public Option/Result constructors

Outside generic design scope

  • generic function values
  • higher-order polymorphism
  • type values
  • type-returning functions
  • type-level #if
  • compile-time type inspection
  • where clauses
  • file-local evidence-backed bound dispatch
  • parameterized aliases
  • partial type application
  • higher-kinded types
  • user const generics beyond fixed collection capacity
  • lifetime parameters
  • specialization
  • associated types

Deferred surfaces may land in future Alpha iterations. Outside-scope surfaces require an explicit language-philosophy change before they can be added.

Related concepts