Spec chapter 07
Generics
Syntax
<T> type parameters work on functions, methods, classes, structs, and interfaces.
Function Max<T: Comparable>(a: T, b: T): T
Return a > b ? a : b
End Function
Class Cache<K, V>
Where K: Hashable & Equatable
Where V: Sendable
Field items: Map<K, V>
End Class
Max(1, 2) // T inferred
Max<Float>(1, 2.5) // explicit- A generic Struct or Class is always written with its type arguments:
Box<Int>, notBox. - A generic Method has type parameters of its own, after its type’s:
Method Map<U>(f: Func(T): U): Stack<U>. It has a copy for each set of type arguments, so it isn’t in the Method table: it can’t beAbstractorOverride, and a call never goes through an override. - A type parameter can’t share a name with a type, or with a type parameter it would hide.
- Generic code is compiled once for each set of type arguments it’s used with, so code that
calls itself, or each other, with a type holding its own type parameter, like
Deep<T>callingDeep([x])with aT[], would need instances without end, and is an error at the call: “‘Deep’ gets T[] for T here, which leads back to this code, so it would need a larger instance each time, without end”. A Const type argument computed from its own parameter needs a different instance each time, asDown<N - 1>does, unless/or%computes it: then it can come back to one already made, asBits<N / 2>comes toBits<0>, which needs itself, so only the chain of instances below stops one that doesn’t. A type that only names a larger instance, like a Field ofNode<Node<T>>?, is fine, and so is making one with the New a Struct gets, when its Fields’ defaults are known at compile time, since that runs none of its code: aQuantity<L + L2>in Quantity’s own Operator. An Interface’s default is code of this kind too, with what each call gives it for Self, and a Class’s copies of the defaults in the Interface’s table run when one of its objects is made, so a default that wraps Self in a Class implementing the Interface,Holder<Self>(Self), is an error where the wrapped one runs it again: a call of it on the Holder, or the Holder’s table holding it. One the compiler can’t follow there, like a Static default, or one that only tests for a Holder, which still makes its table, is an error where the compiler makes the instance that keeps growing: one more than 50 instances of the same function or Class deep in a chain of instances made one from another, or one whose type arguments are made of more than 65536 types, as an argument that doubles each time, likePair<Self, Self>, soon is. A type nested more than 1000 deep, or made of more than 65536 types, is an error even where nothing grows. A Field’s type that grows that way is left out past those limits, as it’s never reached. - A Const type argument an instance works out, like
N * 2orN - 1, has to fit its type parameter’s type, and one that’s a fixed array’s size has to be from 1 to 2147483647, or it’s an error where the instance is made, as one that divides by 0 is.
Inference
A call can leave out its type arguments when the compiler can tell them:
- from the types of the arguments:
First(names)withnames: String[]givesT = String, and an argument whose Class, Struct, or array implements an instance of a generic Interface, or whose Interface extends one, gives that instance’s:Total(set)forvalues: Iterable<T>with aSet<Int>givesT = Int; - from literals passed for a type parameter:
Max(1, 2)givesInt, andMax(1, 2.5)givesFloat, since one of them has a decimal; - from lambdas, once the type arguments their parameters need are known:
Transform(xs, Function(x) => "{x}")takesTfromxsandUfrom the lambda’s result; - from the type expected:
Local s: Stack<Int> = Stack().
A call can also write a generic Function’s or Method’s first type arguments and leave the rest
to inference, as a _ for each of them does in Rust: db.Query<Player>(sql, id) gives
Query<T, A> its T, and its A comes from id. What’s written is taken as given, so an
argument that doesn’t fit it is an error, not inferred instead: Two<Int>("s", 2) wants an Int.
A Method that an Extend of T[] adds counts only its own type parameters, since the array gives
T. An overload with fewer type parameters of its own than a call writes doesn’t fit it, and a
Function or Method that isn’t generic takes none: Plain<Int>(1) is an error. A type’s New still
writes all of a type’s: Box<Int>() for a Box<A, B> is an error.
The arguments of a generic type’s Method come from the object it’s called on. When something
can’t be told, the call is an error that names what it can’t, and asks for the type arguments,
with what it wrote and what it told: “can’t tell what A is in this call; write the type
arguments, like db.Query<Player, Int>(…)”. A Function used as a value takes its type arguments
from the Func expected, or from what’s written, all of them or the first ones: Local f: Func(Int[]): Int = First, or Local g: Func(Int, String): Int = Two<Int>.
When a generic overload and one that isn’t generic fit a call equally well, the one that isn’t generic is chosen.
Overloads that take the same parameter types can still differ in how many type parameters of
their own they have, as Json.Read(text), which gives a Value, and Json.Read<T>(text), which
gives a T, do. A call that writes type arguments can’t pick one with fewer, as above, and a call
that writes none takes a generic one only where its arguments tell its type arguments. A
Function used as a value picks the same way, with the Func expected telling them. A Class’s
Method with another count than one it inherits, or than an Interface’s default, is an overload
beside it, not its Override or its replacement, since a generic Method is in no Method table.
Constraints: inline and Where
Both forms exist, and there is still only one correct way to write any signature. The compiler warns about the other, with a fix-it that an editor offers:
- Inline
<T: X>for a single, simple constraint. Wherefor multiple constraints, multiple constrained parameters, a constraint that relates a type parameter to another through a generic Interface, asS: Iterator<T>does, or anything inline can’t express. A one-line declaration,... => value, or one without a body has no lines for Where lines, so it can have them inline.
& combines constraints: Where K: Hashable & Equatable.
Things only Where can express
Conditional methods, available only when the type argument qualifies:
Class List<T>
Method Sum(): T
Where T: Numeric // ints.Sum() works; enemies.Sum() is a compile error
...
End Method
End ClassConditional conformance:
Extend List<T> Implements Printable
Where T: Printable
Method ToString(): String => "[" + Join(Self, ", ") + "]"
End Extend- An Extend’s Where lines constrain the type parameters it names: everything it adds is conditional, and so is each Interface it lists, with the defaults it gives. Without them, it adds to every instance.
- What a Where line makes conditional implements an Interface only where the Interface is
listed for instances that meet it: a type that lists
Sizedfor every instance implements it with Sized’s default, and the one a conditionalDoubled Extends Sizedgives is what a call through Doubled runs. A conditional Method can’t implement a member of an Interface listed for every instance. A call on an instance that has both reaches the one every instance has, as it would a type’s own Method before an Extend’s, and an Override in a Class extending it overrides that one; a call through Doubled still runs the one the Extend gives, which nothing overrides. - A conditional
Operator ==is what==means for the instances that meet its Where, in a direct==and inside the arrays, tuples, Structs, and T?s that hold them; the others have no Operator == of their own and compare the built-in way. - A Class extending an instance that doesn’t meet a Where, like
Base<Rock>, has none of what it makes conditional, so it can declare Methods and Properties of those names. One extending an instance that meets it, or one with the Class’s own type parameters in it, likeBase<T>, can’t, as for any Extend’s member. - A Class that implements an Interface only through an Extend’s Where is a value of that Interface for the instances whose type arguments meet it: each one’s objects get the table, and the tables of the Interfaces it extends.
Relations between parameters, through a generic Interface:
Function Collect<S, E>(items: S): E[]
Where S: Iterator<E>- A Where line goes at the top of the body. On a generic type, it constrains the type’s type parameters. On a Method, one for its type’s type parameter makes the Method conditional: only the instances whose type argument meets it have it, and it isn’t in the Method table, so a Class extending one of its instances can’t override it, or declare a Method of its name and parameters.
- Each call,
New, and type written with type arguments meets the constraints. A type argument meetsT: Iwhen it’s a Struct or Class that implements I, an Interface that is I or extends it, or a type parameter whose own constraints say so. - A call infers a type argument from a constraint on another one:
Collect(numbers)takesEfrom the instance ofIteratorthat the type ofnumbersimplements.
Class and Error
Class in a constraint says the type argument is a Class, as C#‘s where T : class does, so
generic code can refer to its objects without keeping them:
Function Subscribe<O: Class>(owner: O, handler: Func(O, T)): Subscription
Local kept: Weak<O> = owner
...
End Function- Under it,
Weak<T>and[Weak x]take a T, as they take a Class (see 08), and so do RefEquals,x Is C, andCase c As C, either way round:x Is Ttests for each instance’s Class. - A Class meets it, an instance of a generic Class, and a Shared Class. Nothing else does: not a
Struct, tuple, fixed array, or built-in type, which are values; not a String, a T[], or a
function value, which refer to something that isn’t a Class’s object; not an Interface, whose
value can be an array too; and not a
T?, which can be Null. - It’s only a constraint. It gives what every Class has, which is that it’s
Equatable, and&adds the rest:Where T: Class & Hashable. With Sendable,Where T: Class & Sendable, the T is a Shared Class, so itsWeak<T>is Sendable too.
Error, std’s root of errors, in a constraint says the type argument is a Class that extends
it, as Swift’s E: Error does, so generic code can throw and catch one, and list it after a
function’s Throws or a Task’s: Function Rethrow<E: Error>(error: E): Int Throws E (see
09 and 10).
- A type parameter in a Throws needs it: “E is in a Throws, so it needs a constraint that says it’s an error, like <E: Error>”.
- What it gives is what
Classgives, and it converts to an Error, whose members it has through that:Local error: Error = failed.Catch e As Ecatches each instance’s E.
Constraints are interfaces
Interfaces used as constraints may require statics, operators, and Self:
Interface Numeric
Static Method Zero(): Self
Operator +(a: Self, b: Self): Self
Operator *(a: Self, b: Self): Self
End Interface
Function Sum<T: Numeric>(xs: T[]): T
Local total = T.Zero()
For x In xs
total += x
End For
Return total
End FunctionBuilt-in interfaces
Equatable, Comparable, Hashable, Numeric, Printable, Cloneable, and Defaultable
are in std/core/interfaces.fb, and Iterator<T>, which For ... In goes over, is in
std/core/iterator.fb (see
04-control-flow.md). Sendable, also in std/core/interfaces.fb, says what
can go to another thread, which the compiler decides (see 10-concurrency.md),
and std.ffi’s CData says what C code has, which it decides too (see
12-ffi.md).
Interface Equatable
Operator ==(a: Self, b: Self): Bool
End Interface
Interface Comparable Extends Equatable
Operator <(a: Self, b: Self): Bool
Operator >(a: Self, b: Self): Bool => b < a // and <= and >=, from <
End Interface
Interface Hashable Extends Equatable
Method Hash(): UInt64
End Interface
Interface Printable
Method ToString(): String
End Interface
Interface Numeric
Static Method Zero(): Self
Static Method One(): Self
Operator +(a: Self, b: Self): Self // and -, *, and /
End Interface- The built-in types implement the ones that fit them. Numbers implement all but
Cloneable; Strings all butNumericandCloneable; and Bools and EnumsEquatable,Hashable,Printable, andDefaultable. AT?, tuple, or array implements what its elements do, except that aT?is onlyEquatableandHashable, and neither tuples nor arrays areComparableorNumeric. std.math’s vectors areNumeric,Hashable,Printable, andDefaultable, and its matrices andQuatEquatableandPrintable(see stdlib.md). Cloneable(Method Clone(): Self) is what makes a copy of itself, which arrays are with their ownClone, andDefaultable(Static Method DefaultValue(): Self) what has a value to start from, which the numbers, Bools, Strings, Chars, and Enums are with their zero:T.DefaultValue()is 0, False, "", U+0000, or an Enum’s zero, its first value, or no flags for a @Flags Enum. std’s collection Classes are both.- A built-in type has the Methods of the ones it implements:
5.ToString(),name.Hash(). - A Struct or Class implements one by listing it, and every Class is
EquatableandHashablewithout listing them, soenemies.Remove(e)andSet<Enemy>work on any Class. One that listsEquatablewithout an Operator==of its own gets the built-in one, which compares a Struct’s fields or a Class’s objects themselves, and one that listsHashablewithout aHashgets the built-in one, which combines a Struct’s fields’ hashes or comes from a Class’s object. The built-inHashgoes with the built-in==, so a type with an Operator==of its own needs aHashof its own, however it gets that==: written, inherited, from an Interface’s default, from another file’s Extend, or, for one instance, from an Extend of it. ThatHashisHash(): UInt64; one that gives another type is a Method like any other. A Class with such an==and noHashisEquatablebut notHashable, unless it listsHashable, which is then that error. An instance whose Extend gives it an==decides for itself:Box<String>, given an==and aHash, hashes with them, and given only an==isn’tHashable, whileBox<Int32>hashes its objects. One whose==has a Where line, its own or an Extend’s, implements them as it lists them, as a Struct does. - Values that are equal have equal hashes. A
T?that holds a value hashes like it, and Null’s hash is 0. - What implements
Printablegoes in a string through itsToString:"{enemy}". An Interface’s object does too, when the Interface extendsPrintable, and so does aTwhose constraints say it’sPrintable.
Generic Interfaces
Interface Source<T>
Method Take(): T
Method TakeTwo(): (T, T) => (Take(), Take())
End Interface
Class Counter Implements Source<Int>
...
End Class
Struct Constant<T> Implements Source<T>
Field Value: T
Method Take(): T => Value
End Struct
Interface Labeled Extends Source<String>
ReadOnly Property Label: String
End Interface- An Interface’s type parameters are types in its members, like its Self. Each set of type
arguments makes an instance, like
Source<Int>, whose members have them in their types: a Counter’sTakeTwogives an(Int, Int). - A type implements one instance of a generic Interface at most, counting the ones it lists, the
ones those extend, and the ones it inherits, and for an instance like
Box<Int>, the ones its generic type gives every instance, so its members’ types are always known. A type parameter’s constraints name one instance of each too. - An instance that can be a type, like
Source<Int>, has values like any Interface, and an Interface can extend an instance. - An Interface’s type parameters can have constraints,
Interface Sorted<T: Comparable>, which hold in its members, and which each instance meets: a constraint<S: Sorted<E>>needsE: Comparabletoo.
Const generics
Struct Grid<T, Const W: Int, Const H: Int>
Field cells: T[W, H]
Method Width(): Int => W
End Struct
Function Sum<Const N: Int>(values: Int[N]): Int
Local g: Grid<Float, 64, 64>
Sum(row) // N from row's Int[8]- A Const type parameter,
Const N: Int, stands for an integer of its type: in the body it’s a value of that type, and in types a fixed array’s size, or an argument for another. It has no constraints. - Its argument is an integer literal, a
Const, or a Const type parameter of the code around it, any value of its type, 0 and below among them, as a unit’s exponents are, though aUInt64’s goes up to 9223372036854775807, since each instance works one out in an Int64. Each value makes an instance, likeGrid<Float32, 64, 64>. Where it’s a fixed array’s size, it has to be from 1 to a fixed array’s limit, which each instance checks. - An argument or a fixed array’s size can be computed from Const type parameters with
+,-,*,/, and%, which round toward 0 as an Int’s do, a-before one, and parentheses:Function Join<Const N: Int, Const M: Int>(a: Int[N], b: Int[M]): Int[N + M],Grid<W * 2>, orQuantity<L1 - L2, (T - 1) / 2>. Each instance works it out, in an Int64, and one that divides by 0, passes what an Int64 holds, doesn’t fit its parameter’s type, or makes a size below 1 is an error where the instance is made, asTrimmed<1>is for anInt[N - 1]. - Values computed alike are one value, whatever types they’re reached through:
+,-, and*by a known number combine, soL - 0isL,L + LisL * 2, andM + NisN + M, and each instance works out what they combine to. One that would drop a part that can fail, like(10 / N) * 0, stays as it’s written. - A call infers it from a fixed array’s size:
Sum(row)withrow: Int[8]givesN = 8. - A fixed array whose size is a Const type parameter can’t come from an array literal, since its size is only known in each instance.
- An Interface can have Const type parameters too:
Interface Sized<Const N: Int>.
Semantics
- Monomorphized (like C++ templates): every instantiation compiles to specialized native code. Tradeoff: binary size and compile time.
- Invariant:
List<Boss>is not aList<Enemy>. No variance annotations. - A generic body is checked once, with its type parameters as types of their own, so errors
point at the generic code rather than at each use. What a
Tcan do is what its constraints say: without any, it’s stored, passed, returned, and put inT?,T[], tuples, and Funcs, but it has no members, no==, and no zero, and can’t be shown in a string. With them, it has their Methods and Properties,T.Zero()calls their Static Methods, and their Operators work on it,==among them. It still isn’t an Interface’s value, since it may be a Struct. - Each instance calls its type argument’s own members, so generic code costs what the code it stands for would.
T?with anInt?forTisInt??, so generic code’sNullfor “none” stays apart from aNullit holds:Findover anInt?[]tells an element that’sNullfrom one past the end.
Parsing <
C#‘s disambiguation rule: in expression context, Name<...> is parsed as type arguments
when the token after the closing > is (, ., ), or ,. Otherwise < is a
comparison. Type arguments can be qualified, so F(a < m.B, c > (d)) calls a<m.B, c>, as
F(a < b, c > (d)) does; parenthesize a comparison there: F((a < m.B), c > (d)).
The closing >> of nested type arguments is read as two >s, so Map<K, List<V>> needs no
space.
Report an issue with this page or with FutureBlitz on GitHub.