Generics and macros: one template, many versions
On this page
- Many versions from one template
- Generic functions
- Working out the type arguments
- Inference from where a value goes
- Generic types
- Generic structs, and their functions
- Generic methods
- Several type parameters
- Value parameters: generic over a number
- Generic modules
- Constraints: what a type must be able to do
- A trait as a constraint
- Combining traits
- A generic trait as a constraint
- A function signature as a constraint
- where clauses
- A method that exists only for some instances
- A type that meets a trait for some instances
- Constraints not built yet
- Macros: a fresh copy at every use
- Macros as methods
- Metaprogramming (not built)
Some logic is the same whatever type it works on: the larger of two values, a list, a sort. Writing it once per type means fixing every bug once per type. Instead you write it once, as a template with the type left open, and the compiler makes a version for each type you use it with. This page covers generic functions, types and modules; the constraints that say what a type must be able to do to be used; and macros, which make a fresh copy at every use. Afterwards you can write a piece of logic once and use it with any type that fits.
mod largest;
import stdio use *;
// One template. T stands for a type: the compiler makes a
// version of max for each type it is used with
fn max[T](a T, b T) T {
if a > b {a;} else {b;}
}
// A generic type: a pair of values of any one type
struct Pair[T] {
pub first T;
pub second T;
pub fn larger(self &) T {
max(first, second);
}
}
fn main() {
print <- max(4, 6), " ", max(2.5, 1.5), "\n";
let sizes = new Pair[i64](10i64, 3i64);
print <- sizes.larger(), "\n";
}
It prints:
6 2.5 10
Try this: call max(true, false). Nothing says what > means for a bool, so the version for bool cannot be made; constraints, below, are how a template says so up front.
Many versions from one template#
Generativity makes many concrete components from one template, each obtained by putting in the parameters given where it is wanted. Cone has three kinds:
- Generics make one version for each different set of arguments: max[i32] and max[f32] are two functions, and every use of max[i32] is the same one. Functions, types and modules may be generic.
- Macros make a new version in place at every use, even where the arguments are the same.
- Metaprogramming, which is planned and not built, would decide what logic is compiled at all.
The gain is reuse of one pattern of logic for many types. Because the versions are made when the program is compiled, a generic is the static form of substitution: a call into a generated version is direct, where a call through a virtual reference goes through a table. The cost is coupling: the template, its versions and the code that uses them all depend on one another, which makes a program harder to understand and more fragile under change. Collections are the classic use, since one generic applies the same logic to whatever the collection holds, but the usefulness goes well beyond them: core's own Result and Option are generic enums.
Generic functions#
A generic function is a function with a list of type parameters in square brackets after its name. max[T] above is generic over one type, T, and expects both its parameters, and its result, to be of it. A use may name the type, or leave the compiler to work it out:
let val1 = max[i32](4, 6); // 6 let val2 = max[f32](2.0, 5.0); // 5 let val3 = max(4, 6); // T is i32, from the arguments
In effect the compiler writes a max for i32 and another for f32, putting the type in wherever the template says T. Each version is made where it is first used, once however many times it is used. A generic imported from another package is no different: its body comes with the package's include file, and the program makes the versions it uses. When the package and the program both use the same one, both compiled objects contain it, and linking keeps one.
Working out the type arguments#
When a type argument can be worked out from the arguments' types, it need not be written. An unsuffixed integer literal is whichever number type the other arguments give: max(big, 6) with a big of type i64 infers i64 and types the 6 as one. Only when no other argument says does the literal default to i32. A float literal is not adaptable so: it is an f32 wherever it stands, and an f64 is written 2.5d.
A type parameter may also be found inside a parameter's type. A pointer, reference or slice to T infers T from what the value passed points at; a generic type's instance, such as Box[T], infers each of its type arguments from the instance passed; and a function reference's signature, such as &fn(a A) R, infers each type parameter it names from the signature of the function referenced, parameter by parameter and then the return type:
fn first[T](s &Array[T]) T {s[0];}
fn unbox[T](b &Box[T]) T {b.v;}
fn apply[A, R](arg A, f &fn(a A) R) R {f(arg);}
fn widen(n i32) i64 {i64.from(n) * 1000;}
let nums = [4, 5, 6];
let n = first(&nums); // T is i32
let v = unbox(&boxedf); // T is f32, when boxedf is a Box[f32]
let w = apply(7, &widen); // A is i32 and R is i64
A type parameter that no argument names, but the constraint of one that an argument does, is read off that argument's methods: see A generic trait as a constraint.
Inference from where a value goes#
A type parameter that nothing the call is given says may be said by where the call's value goes. What the generic makes, written in its type parameters, is matched against the type expected of the value, and the parameters still unsaid take what it holds. A function makes its return type, a generic type itself, Box[T], and an enum's variant its enum, Option[T]. A variable's declared type, a return type, the left side of an assignment, a parameter's type, a field's type, the branches of an if and a block's last value all say what is expected:
fn make[T]() Box[T] { new Box[T](0); }
let b Box[i64] = make(); // T is i64
let c Result[i64, i32] = Ok(5); // the error type is i32, from the declared type
fn parse() Option[i64] {
return None; // T is i64, from the return type
}
This is one mechanism for every generic and every enum, not a rule about Option. It only fills the parameters nothing else named: an argument, a bound, or a type argument written in brackets always wins, and an expected type the generic's result does not match says nothing, so the usual mismatch is reported. An unsuffixed integer literal follows the expected type before it takes its default, so let n i64 = add(2000000000, 2000000000) adds i64s. The expected type of a call among the arguments of an overload set is the type every candidate that could take the call declares there, if they all agree. When one operand of == or != is a call, the other operand's type is what it is expected to be, and so is the parameter type of the <- being appended to.
Where nothing gives a parameter, the call is refused, and the message shows how to write the type arguments out: let z = None; is refused with "Could not infer the type parameter T of None, since neither its arguments nor where its value goes say what it is. Write the type arguments out: None[T]."
The expectation reaches inward. A construction given to a construction is wanted as the type its place has, so Some(None) wanted as an Option[Option[i64]] holds a None[i64], and Some(5) there holds an i64. A construction given to a generic function waits for the other arguments, which may say what its place is: in id2(None, 5), with fn id2[T](a Option[T], b T), the 5 makes T an i32 and the None an Option[i32]. And None on the left of == or != is typed as the right side is. A call of a generic function given as an argument is not a construction, and is typed from its own arguments alone.
A variant of an enum given to a generic is typed by what is expected of it too. In return Error(Fault.SoldOut); from a function returning Result[Drink, Fault], the Fault.SoldOut is a Fault, so the error type is Fault, not the variant's own type, as when a variant is passed to a parameter of its enum. The same holds for Ok(Drink.Tea) and Some(Color.Red), wherever the value goes, for a variant that holds a value, Fault.Short(3), and for a variant of a generic enum of your own.
Where nothing says, a generic variant's type arguments are written in brackets after its name, and its values after them: None[i32], Some[i32](5). Square brackets after a variant give type arguments and never values, so Some[5] and None[] are refused, naming the spelling that replaces them ("A variant is constructed with parentheses: write Some(...), not Some[...]"). In a pattern or after is a name stays a type: case is None[IoBuf]. An empty variant's name is a value, so a method may be called on it directly, Lamp.Red.get(), as (Lamp.Red).get(); only a method that takes self reads this way, and a variant that holds values is not a value by its name alone, so Shape.Circle.area() is refused for the self it lacks. A generic enum's empty variant has nothing to take its type arguments from as a receiver, so they are written: Pending.Waiting[i64].key().
Generic types#
A generic type looks like any type declaration, with the list of types it is generic over after its name. Core declares Option this way, as a generic enum:
pub enum Option[T] {
None;
Some(T);
}
Its variants are names of the enum, so they are reached through it, Option.Some(7), and a generic variant is instantiated exactly as it would be named bare. Some and None are bare in every program because core folds them in with pub use Option; (Naming a variant). A variant is constructed with parentheses, Some(5), and an empty one by its name alone, None. One declaration serves every type:
var anint Option[i32]; var someint Option[i32]; var afloat Option[f32];
anint and someint have the same type, an integer that may be missing, and afloat's type is different. Once declared, a generic type may be used anywhere any other type may, including as a type argument of another generic. It is always named with its type arguments: Option alone is not a type, only Option[i32] and its other instances are, except inside its own braces, where its bare name is the instance being defined.
So inside struct Box[T] { ... }, Box means Box[T]: in Box[i64] it is Box[i64], in Box[f32] it is Box[f32]. A generic enum's braces take in its methods and every variant's body, and there a variant's bare name, None or Option.None, is that variant at the enum's current arguments too:
struct Box[T] {
pub v T;
fn again(self &) Box {new Box(v);} // Box[T], built from a T
fn other(self &) Box[i32] {new Box[i32](7);} // any other instance, in full
}
enum Maybe[T] {
struct Yes {
pub value T;
fn drop(self &) Maybe {Maybe.No;} // Maybe[T] and Maybe.No[T]
}
struct No {}
}
Any other instance is still written in full, even inside the braces, and even a use of the generic's own name given type arguments names the instance those arguments say: Box[i32] inside Box[T] is Box[i32], and Box[T] is the same instance as the bare Box. Another generic named bare inside is no more a type there than anywhere else. A generic enum may also extend another enum, generic or not, and be extended: see Extending an enum.
Generic structs, and their functions#
A generic struct is declared the same way, as this beginning of a resizeable collection is:
struct Buffer[T] {
pub ptr *T;
pub used usize;
pub cap usize;
}
var string Buffer[u8];
var pointers Buffer[Uni[Node]];
A generic type's functions are reached through an instance, as any type's are through its name: List[u8].empty() calls List[u8]'s own empty. A function or static variable of the generic itself cannot be named, as List.empty(): only each instance has them. Reaching a static variable through an instance is not yet implemented.
Generic methods#
A type, generic or not, may have generic methods, which look and act much like generic functions. The type arguments follow the method's name, wherever it is written, and may be left out where the arguments after self infer them:
struct Holder {
pub k i64;
pub fn pick[T](self &, v T) T {v;}
fn twice(self &) i64 {pick(2i64) + pick[i64](3);} // self.pick
}
let h = new Holder(1);
let a = h.pick(6i32); // T is i32
let b = h.pick[i64](7);
let c = Holder.pick(&h, 8i32); // the receiver passed, T is i32
A generic type's method may be generic too, over type parameters of its own: each instance of the type has the method, and each makes its own versions of it, as Box[i64] makes pair[f32] for bx.pair[f32](1.5).
Several type parameters#
A generic type or function may take more than one type. Core's Result is declared like this one, which takes another name so as not to collide with it:
enum Outcome[T, E] {
Good(T);
Bad(E);
}
struct BadArith { pub code i32; }
var i32OrBad Outcome[i32, BadArith] = Outcome.Good(3);
Value parameters: generic over a number#
A generic function or method may be generic over a number as well as over types. The parameter is named, then given an integer type, in the place a type parameter's constraint goes:
fn dot[N usize](a &Array[f32, N], b &Array[f32, N]) f32 {
var sum = 0.0;
each i in 0usize ..< N { sum = sum + a[i] * b[i]; }
sum;
}
The parameter stands for an integer literal of its type, in the way a type parameter stands for a type. It is used only as itself: as an array's size, Array[T, N], or as a value in the body, here the loop's limit. The compiler makes an instance for each number, as it does for each type, so dot over arrays of 3 and of 4 are two functions, each as fast as one written for that size. The number is inferred from an argument whose type has the size, or written in the brackets after the name, beside any type parameters in the order declared. An inferred 3 and a written 3 are the same instance:
let a = new Array[f32, 3] <- 1.0, 2.0, 3.0;
let b = new Array[f32, 3] <- 4.0, 5.0, 6.0;
let x = dot(&a, &b); // N is 3, read off both arrays: 32
let y = dot[3](&a, &b); // the same instance
fn filled[T, N usize](v T) Array[T, N] {
new Array[T, N] <- fill v;
}
let z = filled[i32, 4](7); // four 7s
A written number is an integer literal, not negative, that the parameter's type holds and, with a suffix, of that type. It is no variable, because the compiler decides it when it makes the instance. A number inferred from two arguments must agree: dot(&a3, &a4) is refused, naming both sizes. The size may be that of a slice's elements, and nested arrays give one number each:
fn width[T, N usize](rows &Array[Array[T, N]]) usize {N;}
fn cells[N usize, M usize](g &Array[f32, N, M]) usize {N * 100usize + M;}
A generic type's method may take a number of its own, fn chunk[N usize](self &), called buf.chunk[3](). A field of the same name as such a method is still indexed, s.pick[2]. The first user is core's asArrays[N](), a method of an array and a slice that sees its elements as rows of N (Seen as rows).
Not built, and refused: arithmetic over the parameter in a type, Array[T, N + 1], which would need two such sizes compared; a number parameter on a generic type, struct Matrix[N usize]; a parameter that is not an integer (f32, bool and char are not); a constraint beside the integer type, and a where clause about it. A parameter is not a type, so it names no type of a value's: x N is not a parameter's type.
Generic modules#
A module may be generic, instantiated with type arguments where it is used, each instance with its own globals. Type parameters only: parameters with constraints or of other kinds are not implemented. A generic module may not yet hold a submodule, a generic function or type, a trait or an enum, a macro, a module trait, or a global with a use clause, and it may not be C-named.
A module may be generic, written as a generic type is: its type parameters in square brackets after its name on the mod line. Here it is a one-file submodule of a program:
// src/stack.cone
pub mod stack[T];
pub var @threadlocal count i64 = 0i64;
pub var @threadlocal top T = 0;
fn @initpure init() {
// runs once for each instance
}
pub fn push(x T) {
top = x;
count = count + 1i64;
}
It is used through an instance, named with type arguments where it is used, as Box[i64] names an instance of a generic type:
fn main() {
stack[i64].push(3i64);
stack[f64].push(1.5d);
let n = stack[i64].count; // 1
}
- Each instance is a module of its own, with its own global variables and its own initializer and finalizer: stack[i64].count and stack[f64].count are two globals.
- The same arguments are the same instance, wherever they are written: stack[i64] named in two modules is one module with one set of globals.
- Nothing of a generic module is compiled until an instance is named. It has no members of its own to reach, so its name without type arguments names no member, and nothing of it can be folded or extended: stack.push, use stack;, import stack use * and extends stack are errors. Inside its own body, its name is the instance being defined: stack.count there is that instance's global.
- An instance initializes after its generic's dependencies and after the modules its type arguments come from, and finalizes in the reverse. Given a type of the module that uses it, the instance's initializer runs after that module's.
- Across packages, a package's include file carries its generic module's whole source, and each package or program that uses an instance compiles it; the copies merge when the program is linked, so an instance still has one set of globals. A library's root module may be generic; a program's may not.
- A generic module conforms to a module trait on its mod line, as any module does, and each instance is checked against it.
Constraints: what a type must be able to do#
A template that uses + or calls a method works only for types that have them, and without saying so the mistake is found deep inside the template, wherever the version for the wrong type fails. A constraint is an early warning: it says in the signature what a type must be, so a use with the wrong type is refused where it is written.
A trait as a constraint#
The simplest constraint names a trait after the type parameter:
fn twice[T Integer](x T) T {
x + x;
}
trait Measured {
pub fn size(self &) i64;
}
fn total[T Measured](a &T, b &T) i64 {
a.size() + b.size();
}
A constraint says what T is, the same question p is Mobile asks of a value and struct Node is Id answers for a type (Traits). It is answered once the type arguments are known, by asking it of them; it is never used to work out a type argument the call did not supply.
- A trait with members is met by any type that fits it, whether or not it declares is Measured: a struct with a size method of that signature is Measured. A type declaring it is one too.
- A marker trait, one requiring nothing of a value, is met only by a type that declares it with is, or that the compiler grants it to: every type is exactly one of Move and Copy; Integer is granted to the integer types, i8 to i64, u8 to u64, isize and usize; Pointer to the raw pointer types, *T whatever T, and not to a reference; Sendable to every type whose values may cross to another thread (Race condition safety); Shareable to every type a borrow of which several threads may hold at once (Sendable and Shareable); and Sized and DynSized to a type by its size (below). Immutable is declared, never granted: str declares it, and a struct may (Immutable). A struct identical to one declaring a marker, but not declaring it, does not meet it.
Size. Sized is granted to a type whose size is known when the program is compiled, which a value may be, and which a reference reaches by one pointer. DynSized is granted to those and to a type whose size a reference to it carries: a trait, whose virtual reference carries a vtable, and str, a body of bytes, and Array[T], a body of Ts, whose references carry their count (dynamically sized bodies). A type declared @opaque, and an @unsized enum, are neither: a reference to one is a single pointer that tells nothing of its size. Neither marker can be declared by a type (is Sized is refused). A name that is one of these, or str, is the compiler's in every module: a module declaring its own is refused as a duplicate name.
With T constrained, a wrong type is refused where it is used, with nothing reported from inside the template. twice(2.5) says "twice requires T is Integer, and f32 is not Integer." A generic type is refused the same way, wherever an instance of it is named: Num[f32] for a struct Num[T Integer].
In a trait's requirement, Self stands for the type meeting it (Self in a trait), so a trait can ask for a method taking another value of the same type:
trait Comparable {
pub fn `>`(self &, b &Self) bool;
}
fn max[T Comparable](a &T, b &T) &T {
if a > b {a;} else {b;}
}
struct Money {
pub cents i64;
pub fn `>`(self &, b &Self) bool {cents > b.cents;}
}
let low = new Money(250);
let high = new Money(475);
max(&low, &high).cents; // 475
Money is Comparable: its > takes a &Money where the trait's takes a &Self. Only a struct fits a trait with members yet: a number type meets the markers granted to it and no trait of your own, so this max cannot yet be called on two i32 values ("max requires T is Comparable, and i32 is not Comparable").
Combining traits#
Several traits may constrain one type parameter, joined with +; the type must meet them all:
fn fill[T Integer + Copy](dst &mut Array[T], v T) {
each i in 0usize ..< dst.len() { dst[i] = v; }
}
A generic trait as a constraint#
A trait with type parameters of its own is a constraint when it is given its type arguments, which may be the function's other type parameters or types. It is written in the square brackets or in a where clause (below), as for any trait:
trait Stack[T] {
pub fn push(self &mut, v T);
pub fn pop(self &mut) Option[T];
}
trait Peek[T] {
pub fn peek(self &) Option[T];
}
fn drain[T, S Stack[T]](s &mut S) Option[T] {s.pop();}
fn fillTwo[S Stack[i64]](s &mut S, a i64, b i64) {s.push(a); s.push(b);}
fn peeked[T, S](s &mut S) Option[T] where S is Stack[T] and S is Peek[T] {s.peek();}
fn both[T, S Stack[T] + Peek[T]](s &mut S) {}
A type fits Stack[i64] as it fits any trait, by having the methods the trait requires, here push(self &mut, v i64) and pop(self &mut) Option[i64], the trait's signatures with T replaced by i64; or by saying is Stack[i64]. Stack[i64] and Stack[f64] are different traits, so one type may fit both. Whether it does is decided where the function is used, for the arguments it is given. A type that does not fit is refused there, naming the trait at the arguments of that use, the method it lacks or has with another signature, and the signature required:
// WrongParm has push(self &mut, v bool) drain[i64, WrongParm](&mut wp);
Error 1186: drain requires S is Stack[i64], and WrongParm is not Stack[i64]. WrongParm has push(self &mut, v bool), but Stack[i64] requires push(self &mut, v i64).
The arguments may be written, drain[i64, IntSlot](&mut box), or inferred. The type parameter S is inferred from the argument as always. T, which no argument's type names, is then read off the methods S has: each parameter type and the return type of the trait's methods are matched with those of the same-named method of the type, so drain(&mut box) finds i64 in IntSlot's pop. It fills only what is still empty, and the constraint is then decided as above, so a type whose methods disagree is refused with the message above. A type parameter another argument names is inferred from that argument first, except an unsuffixed integer literal passed for a bare T, which is whichever number type is wanted and names T only if nothing else does: fill(&mut box, 8) types the 8 as an i64 when the stack is an i64 box. A constraint names a generic trait only with all its type arguments, so [S Stack] is refused. A bound may name any parameter of the declaration, written before it or after: [T, S Stack[T]] and [S Stack[T], T] say the same.
A function signature as a constraint#
A function signature written bare, fn(a &T, b &T) i32, is a constraint too, one that needs no trait declared: the type has a pub () method whose parameters after its receiver, and whose return type, meet those (the parameters' names do not matter, and a parameter may be written as its type alone, fn(&T, &T) i32). A signature is met as a function reference's is: by its own types, or by parameters that are supertypes of the signature's and a result that is a subtype, where the difference is a recast with no conversion. It is written in the square brackets or in a where clause, and joins other constraints with + or and. The generic's body calls the parameter directly, order(x, y), in each instance, and the permission it takes the parameter by decides what that call may do: &F reaches a () that takes self &, &mut F one that takes self &mut too, and keeps what it changed, and F by value one that takes self &. A callable whose () takes self &mut, such as a closure that changes a variable it borrows, given to a parameter taken as &F is refused at the caller's argument, which says to take the parameter as &mut F or to pass a callable that only reads.
// The bodies call order(x, y), and work(), as a function is called
fn sortBy[T, F fn(a &T, b &T) i32](xs &mut Array[T], order &F) {}
fn counted[T, F](xs &mut Array[T], order &mut F) where F is fn(a &T, b &T) i32 {}
fn launch[F fn() + Sendable](work &mut F) {}
fn byAge(a &Person, b &Person) i32 { a.age - b.age; }
struct Descending { pub fn `()`(self &, a &Person, b &Person) i32 { b.age - a.age; } }
sortBy(&mut people, &byAge); // a plain function, referenced, meets it as it is
sortBy(&mut people, &new Descending()); // so does a struct with that '()'
sortBy(&mut people, (a, b) => a.age - b.age); // so does a closure written there
A closure literal given to such a parameter takes its parameters' types from the signature, with the type parameters the call's other arguments name read off them (T above is Person from people), so the short form (a, b) => ..., and the full form with a parameter's type left out, both work; written with its types, it needs nothing from the signature. For &F and &mut F the literal is lent as a temporary of the statement. A signature's result may be a type parameter nothing else settles (fn map[T, U, F fn(x T) U](v T, f F) U): the literal's parameters are typed from T and its body gives U, so map(7, x => x * 3) needs nothing written. A &<fn(x T) U parameter cannot do this, since nothing then reads U off the closure.
A type parameter that only the signature names is read off the argument's function or (), as a generic trait's is: fn make[T, F fn() T](f &F) T given &seven finds the T that seven returns. A type that does not meet the signature is refused where the generic is used, naming the signature as written and as it comes to at those arguments, and what the argument has instead: a function of another signature, no (), one that is not pub, or one with other parameters or another return type. The constraint is no &fn(sig) pointer: that stays the one-word address of a function, and a function referenced meets the constraint by its signature alone: through &F, F is the function's type, and taken by value (f F) it is the reference itself, &fn(i32) i32. A plain function has no state to change, so &mut F is for a struct or a closure. A signature is not a constraint on a macro's or a generic module's parameter, nor in a condition on an is entry.
The constraint is the static form of taking a callable: the generic is compiled for each callable it is given, and the call is inlined. The dynamic form, &<fn(sig) (Closure references), is compiled once and calls through the reference, which is what a field or a variable holding a callable of unknown shape needs. A bound may carry the markers a thread or a parallel loop asks, [F fn() + Sendable] and [F fn() + Shareable].
Generic overloads#
A generic function or method whose every type parameter is bound this way may declare an overload name, and so share it with functions that take a number or another kind of callable. For the choice, the generic's &F (or &mut F, or F) parameter counts as a callable parameter of its signature, and the call picks the overload by what it is given:
fn taperNum overload taper(m i32, t f32) f32 { f32.from(m) * t; }
fn taperOne[F fn(u f32) f32] overload taper(m i32, f &F) f32 { f(f32.from(m)); }
fn taperTwo[F fn(u f32, v f32) f32] overload taper(m i32, f &F) f32 { f(f32.from(m), 2.); }
taper(2, 0.5); // the number: 1
let k = 3.;
taper(2, t => t * k); // taperOne: 6
taper(2, (a, b) => a + b); // taperTwo: 4
taper(2, &new Doubler()); // a struct whose '()' is (u f32) f32: taperOne, 4
A closure literal is matched in steps. It can meet only the overloads whose parameter there is a callable (a number never takes it). Of those, the generics the call's other arguments leave viable, then the ones that take as many parameters as the literal writes, then, if several remain, the ones whose signature has exactly the parameter types the literal writes. One left is chosen; none or more than one is an error that names the candidates and says to write the parameter's types, taper(2, fn(u f32) f32 { u; }). The body is never tried against the candidates to see which fits. A function reference or a struct is matched by its exact signature, and two generics it meets are an ambiguity error naming both.
A generic with a type parameter that is not bound by a signature, such as [T] or [T Comparable], may not declare an overload name; the error names the unbound parameter. Once the overload is chosen it stands: if its instance then fails to type check, the call does not fall back to another overload. A generic method joins a shared name on the same terms as a generic function: when every type parameter is bound to a function signature.
where clauses#
The same constraints may instead be written as a where clause just before the block, after a function's signature or after a type's name and its is list. Each clause is T is Name, clauses are joined with and, and a clause may join traits with + as the square brackets do. These declarations say exactly what their bracketed forms say:
fn twice[T](x T) T where T is Integer {
x + x;
}
fn fill[T](dst &mut Array[T], v T) where T is Integer and T is Copy {
each i in 0usize ..< dst.len() { dst[i] = v; }
}
struct Num[T] where T is Integer {
pub v T;
}
A where clause may also offer a choice, which the square brackets cannot: clauses joined with or are met when any of them is. and binds tighter than or, as in an expression, and parentheses group. A clause may name a type instead of a trait, T is bool, met by that type alone, and the type may be written out as an instance: K is Uni[str], or U is Option[T] (met when U is the Option of whatever T is). Core declares its atomic values so: pub struct Atomic[T] is AtomicValue, Move where T is Integer or T is bool or T is Pointer. With three traits of your own, A, B and C:
fn loose[T](x &T) where T is A and T is B or T is C {} // (A and B) or C
fn tight[T](x &T) where T is A and (T is B or T is C) {} // A and (B or C)
The whole clause is evaluated once the type arguments are known, and a use whose arguments make it false is refused, naming it whole: "Atomic requires T is Integer or T is bool or T is Pointer, and it is false for T = f64." not is not built, and neither is or in the square brackets, where every trait named is required: [T A or B] is refused, and is written [T] ... where T is A or T is B.
A where clause also orders lifetimes (Lifetime annotations): 'a >= 'b says 'a lasts at least as long as 'b, and 'a == 'b each as long as the other. Such a clause may be written on any function, generic or not, over the lifetimes its signature names, and on a struct over the lifetimes it declares, and joins lifetime and type clauses by and:
fn pick[T](x &'a T, y &'b T) &'b T where 'a >= 'b and T is Copy {
x;
}
A lifetime comparison is never an alternative of an or, nor under a not: a lifetime is never instanced, so no instance settles which alternative holds, and the body could rely on none of them. Clauses over types keep their or.
A generic function's type parameter may carry a lifetime bound, T + 'a: any borrow inside a value of type T lives at least as long as 'a, a lifetime the signature names (Lifetime annotations). It is written in the square brackets after the parameter or after its traits, and in a where clause as T + 'a, or after the traits of a T is clause, as the brackets write it. Each pair of declarations says the same:
fn put[T + 'a](d &mut &'a R, x T) {}
fn put[T](d &mut &'a R, x T) where T + 'a {}
fn post[T Show + 'a](d &mut &'a R, x T) {}
fn post[T](d &mut &'a R, x T) where T is Show + 'a {}
A bound alone is T + 'a, not T is 'a, since T is no lifetime. A bound joins the rest of a where clause by and only, as a lifetime comparison does, and an or or a not over one is refused. A generic type's parameter takes no bound yet.
A method that exists only for some instances#
On a method of a generic type, a where clause over the type's parameters is not a requirement but a condition for the method existing. An instance whose type arguments do not meet it is a perfectly good type that lacks the method: nothing of the method is checked or generated for it, and calling it is the error.
struct Cell[T] {
pub v T;
pub fn load(self &) T {v;}
pub fn add(self &mut, n T) T where T is Integer {
v = v + n;
v;
}
}
var ci = new Cell[i32](3);
var cb = new Cell[bool](true);
ci.add(4); // 7
cb.load(); // true
cb.add(true); // ERROR
Error 1187: No method add for Cell[bool]: it exists only where T is Integer, and bool is not Integer.
Because the method is absent, an instance fits only the traits it has the members for: Cell[i32] fits a trait requiring add, and Cell[bool] does not, whether the trait is asked for by a constraint or by a virtual reference. A generic method's own where clause over its own type parameters is a requirement, as a generic function's is. One joining the type's parameter and its own with or, where T is Integer or U is bool, cannot be decided until both are known, so the method exists in every instance of the type, and the whole clause is a requirement where the method's instance is made.
A condition joined by or works the same way. Core's Atomic[T] (Atomic values) has an add where T is Integer and a bitOr where T is Integer or T is bool, so an Atomic[bool] has bitOr and no add, and an Atomic[*Node] has neither.
A type that meets a trait for some instances#
One entry of a generic type's is list may carry a condition, written after the word if. Each instance decides it with its own type arguments: an instance meeting the condition has the entry, as if it had been written without one, and an instance not meeting it lacks the entry. The condition is written as a where clause's is, and asks only about the type's own type parameters. The plainest use is a future, which must move when what it holds moves, and copy when that copies (Move for some instances):
struct Cell[T] {
pub v T;
}
struct Future[T] is Move if T is Move {
pub c Rc[imm, Cell[T]];
}
fn moves[X](x &X) i32 where X is Move { 1; }
moves(&futureOfHandle); // fine: Future[Handle] is Move
moves(&futureOfI32); // ERROR
Error 1186: moves requires X is Move, and Future[i32] is not Move. Future is Move only where T is Move, and that is false for T = i32.
In plain words: Future[T] is a move type exactly when T is, and a generic function asking where X is Move gets the answer for the instance it is given. The same works for any trait. A marker trait, one requiring nothing of a value, is met only by a type declaring it, so a wrapper passes it on by declaring it under the same condition:
trait Checked {}
struct Wrapper[T] is Checked if T is Checked {
pub v T;
}
A trait with methods is met where the instance has them. Its methods are written with the same condition as a where clause (A method that exists only for some instances), so an instance lacking the entry lacks them too:
trait Show {
fn show(self &) i64;
}
struct Labelled[T] is Show if T is Show {
pub v T;
pub fn show(self &) i64 where T is Show {
v.show() + 100;
}
}
Such a trait is still met structurally as well: an instance that has every method it requires fits it whatever its is list says, as any type does. is Sendable if T is Sendable declares an instance safe to send to another thread, on the type's word, only where its argument is.
An entry with a condition is never the type's base, whichever position it is written in, since what a type stands on does not vary by instance. Copy takes no condition: a type copies wherever nothing makes it move, so the condition is written on Move. A type with no type parameters takes none, having nothing to vary. A condition on an entry of a trait's is list, which a trait passes on to every type implementing it, and on an enum's variant, which is made with its enum, is not built: it is refused. A where clause after the is list keeps its own meaning, a requirement on the whole type: struct Future[T] is Move where T is Move is Move for every instance and refuses Future[i32].
Constraints not built yet#
Not built yet: a where clause is meant to be able to say more than T is Name: a relationship between two type parameters, and a constraint on a type expression rather than a parameter. struct Xyz[T, Y] where T < Y and Option[T] is Node; would say that T is a supertype of Y and that an optional T is a valid Node. Neither form is built, nor not, nor a constraint on a generic module's or a macro's parameters, nor a condition on an is entry of a trait or of an enum's variant, nor a lifetime bound on a generic type's parameter. A generic's body is checked only in each instance, as it always has been, not against its constraints where it is declared. When it is, a body under an or will be held to what every alternative supports: under where T is Integer or T is bool, a body may use what integers and bool both have, such as ^, and not +, which only one of them has.
Macros: a fresh copy at every use#
A generic makes one version per set of type arguments, and every call of it is a call. A macro is closer to the source: its logic is written into the program in place, wherever it is used, with the arguments put in wherever its parameters appear.
// A macro that evaluates to the largest value
macro max[a, b] {
let aa = a;
let bb = b;
if aa > bb {aa;} else {bb;}
}
fn tester(x i32, y i32) {
let maxnbr = max(x, y);
}
A macro's logic must be syntactically complete and valid. Macros are hygienic: names defined in the macro are out of reach of the code where it is used, and the other way round. max captures its parameters into variables before using them so that each argument's expression is evaluated once; written without that, each would be evaluated twice, possibly to different values. The other side of that is an argument that is never evaluated at all, when the macro's logic does not reach it. Core's assertDebug (Panics) is a macro for that reason: in a release build its condition is not evaluated.
Written in a macro's logic, srcFile() and srcLine() answer where the macro is used, not where its logic is written; in a macro used inside another's logic, where the outermost one is used. A macro may take no default argument.
A macro need not declare any parameters. Then its name is the whole of its use, and expands to the macro's logic wherever a value is expected: as an initializer, as either operand of an operator, as an argument, as what a function returns, and as the value a block hands back. Writing an empty argument list, two(), means exactly the same thing.
// A macro that evaluates to the same number every time
macro two { 2; }
fn tester(x i32) i32 {
let a = two; // an initializer
let b = two + x; // either operand of an operator
two * b; // the value the function hands back
}
Macros as methods#
A macro may be declared inside a struct, trait or enum, where it becomes a member of that type. As with a function, it is a method when its first parameter is named self, and it is used like a method, with the receiver before the dot and any further arguments in parentheses:
enum Maybe {
pub struct Nothing {}
pub struct Just {value i32;}
// The value held, or a default when nothing is
pub macro orElse[self, dflt] {
let s = self;
match s {
case let j &Just {j.value;}
case let n &Nothing {dflt;}
}
}
}
fn tester(m &imm Maybe) i32 {
m.orElse(0);
}
The receiver stands in for self exactly as an argument stands in for any other macro parameter: it is a piece of source, put in wherever self appears in the macro's logic, and evaluated as many times as it appears. That is why orElse captures it into a variable first. A macro method with no parameters beyond self is used as the bare member name, m.isSet, or with an empty argument list.
Inside the macro's logic, the type's fields and methods are reached through self, as in self.value, and never by their bare names. The logic expands into whichever function uses the macro, so it may reach the type's private members through self just as a method would, wherever it is used. Inside a method of the same type, a macro method may itself be named bare, as a method may, and means self.name.
Metaprogramming (not built)#
Not built yet: none of this is implemented. The compiler reports a word beginning with # and ignores the rest of its line.
Metaprogramming would give fine-grained control over what program logic gets compiled: including or leaving out certain code, and generating data or logic by algorithm. In effect it is a second, similar-looking language, its constructs marked with #, that manipulates the program's logic as data. Meta-conditionals are control-flow blocks that decide what logic gets compiled, as ordinary ones decide what gets run, and inside generics or macros they are evaluated when an instance is made.
Meta variables would be immutable and exist only while the program is compiled, holding typed constant values calculated from an expression: let #arrsize = 10;. Some would have their values set by the compiler, giving information about the compile-time environment, such as the target platform and architecture (Platforms and targets), and a compiler option could pass data into a program in others.
#if would decide whether to include specific logic in the compiled program, and #each would generate the same logic once for each entry of a collection:
#if #target == "windows" {
... Windows-specific logic ...
}
#elif #target == "linux" {
... Linux-specific logic ...
}
#each #x in #table {
... some logic ...
}
You can now write logic once for many types, say up front what those types must be able to do, and generate code in place with a macro. That completes the part: from dividing a program into modules, to folding names, to traits and generics, you have the means to build large programs from parts that can each change on their own. Next, Safety: what Cone guarantees, where trust comes in, and how a program reaches code written in C.