Types and generics¶
Static types describe both individual values and relationships between values. Protocols express capabilities, while repeated type parameters preserve a shared type choice.
Design documentation
This chapter describes the language design. Examples are illustrative; implementation availability and unresolved rules are explained in Design status.
Choosing between a union and a type parameter¶
A union describes the alternatives a single value may have. Separate union-typed parameters allow separate choices. A repeated type parameter ties several positions to one selection.
For the finite int or float choice below, (int, int) and (float, float)
are permitted argument pairs; (int, float) is not. For two int | float
parameters, that mixed pair is permitted by the parameter types. The body
still has to implement the combinations it accepts.
Type vocabulary¶
| Concept | Meaning |
|---|---|
| Nominal class | A type identified by its declaration, rather than only its shape |
| Structural protocol | An interface described by required capabilities |
| Generic | A declaration parameterized by types and their relationships |
| Union | A value may have any one of several listed types |
| Finite type choice | One invocation selects a concrete alternative used consistently |
| Recursive type | A type definition refers to itself through its components |
| Variance | The rules for substituting related types in a generic interface |
Supported direction¶
Arbor takes a pragmatic approach to static typing: nominal classes, structural protocols, generics, complex and recursive types, and enums are required. The working proposal also includes unions, intersections, tagged enums with payloads, and an explicit dynamic boundary; their complete rules remain to be specified.
type Json = null | bool | int | decimal | string | list[Json] | map[string, Json]
This is an illustrative recursive type. The actual JSON number representation must account for large numbers and remains part of the numeric/data-format work.
Unions versus correlated choices¶
An ordinary union permits arguments to choose independently:
type Number = int | float
fun foo(x: Number, y: Number) -> Number { ... }
A finite type choice preserves the relationship between arguments and result:
choice T = int or float
fun foo(x: T, y: T) -> T { ... }
The choice syntax is provisional. Its meaning is the pair of signatures:
fun foo(x: int, y: int) -> int
fun foo(x: float, y: float) -> float
Each invocation selects one concrete alternative. Every occurrence of T uses
that alternative; the compiler must not choose T = int | float to make a mixed
call fit. A shared body is checked for every alternative. Separate overload bodies
are also supported when implementations differ. Contracts and exceptions belong
to the selected alternative.
Repeated type parameters express relationships in general generics too:
fun find_latest_by_updated[K: Hashable](
table: Table[K],
keys: list[K],
) -> null Row[K] {
...
}
Integer-keyed tables require integer keys; string-keyed tables require string keys. No silent conversion or widening defeats the relationship. Distinct parameters allow independent choices. Nominal key types can further distinguish identifiers with the same underlying representation.
Union-typed inputs need narrowing unless every possible combination is supported. Ambiguous calls are compilation errors. Protocol bounds and finite choices should remain distinct concepts even if the final syntax unifies their notation.
Variance and mutation capabilities¶
Full mutable containers remain invariant. However, the compiler should eventually understand verified restrictions on mutation:
fun duplicate[K](x: mut list[K]) {
let original_length = x.length
for i in 0..<original_length {
x.append(x[i])
}
}
Distinguish preserving an element type K from the stronger promise of using only
elements already present. Reordering, removing, and duplicating existing values
can preserve an unknown element type without granting arbitrary insertion rights.
Capability annotations must be checked, not blindly trusted.
class StatementParser[S: Statement] {
statements: list[S]
}
class CreditCardStatementParser
extends StatementParser[CreditCardStatement] {
...
}
Inherited operations must remain valid under specialization. Narrowing a mutable field cannot leave an inherited method able to insert a broader type. Read-only interfaces can be covariant; restricted mutable interfaces may expose proven type-preserving operations.
v0.1 boundary: conservative rejection is acceptable. The language should leave room for more capable verification later without making current mutable-container rules unsound. Exact capability syntax and proof machinery are open.