Cone Language Reference

Generics

On this page

Note: Not implemented: a generic type that takes a number (struct Matrix[N usize]) and arithmetic over a number in a type (Array[T, N + 1]); a condition on an is entry of a trait or of an enum's variant; a lifetime bound on a generic type's parameter; a clause relating two parameters other than by a generic trait's instance, or constraining a type expression; not in a where clause; constraints on a generic module's or a macro's parameters; and checking a generic's body against its constraints where it is declared, rather than in each instance.

Generics enable the creation of generic function or type logic that works across all types that conform to specified constraints. The compiler automatically specializes this logic to fit every type you apply to the generic code. Generics improve programmer productivity by making it easier to define and use reproducible, type-flexible logic.

Collection types such as lists and dictionaries are well served by this approach, as a single generic can apply the same algorithmic logic across all types the collection stores or indexes with. The usefulness of generics extends well beyond collections. For example, the built-in Result and Option types are also implemented using generics.

Generic Functions#

A generic function definition resembles a regular function definition, just adding a list of type names it is generic over. For example, this simple generic function returns the larger of two values:

fn max[T](a T, b T) T {
  if a > b {a;} else {b;}
}

The use of square brackets after the function name indicate this is a generic function. T represents some arbitrary type. In this example, the function expects that both its parameters, and its return value, have a value of this type.

We can use this function on values of different types:

imm val1 = max[i32](4, 6);      // 10
imm val2 = max[f32](2.0, 5.0);  // 7.0

In effect, the compiler will create multiple max functions, substituting the specified type everywhere the generic function made use of the type T. In this case, it will create a max function that works with i32 values and another that works with f32 values.

Each is made where it is 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.

Generic Type Inference#

When using a generic function, we do not need to specify the type for T if it can easily be inferred based on the type(s) of the parameter values. Thus, this simple use is possible:

imm val1 = max(4, 6);

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.5f64.

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;}

imm nums = [4, 5, 6];
imm n = first(&nums);     // T is i32
imm v = unbox(&boxedf);   // T is f32, when boxedf is a Box[f32]
imm 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] {...}

imm b Box[i64] = make();         // T is i64
imm 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 imm 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 names the parameters and shows how to write the type arguments out: imm z = None; says None[T] is wanted. A constructor given to another constructor, Some(None), a generic whose parameter comes from a later argument, id2(None, 5), and None as the receiver of a comparison are not inferred.

Generic Types#

Generic types work similarly to generic functions. They look like regular type definitions but specify a list of types they are generic over.

For example, one can define something like the built-in Option type using a generic enum:

enum Option[T] {
  pub struct Some {
    pub value T;
  }
  pub struct None {
  }
}

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. The built-in Some and None are bare in every program because the core library folds them in with pub use Option; (see Naming a variant).

A variant is constructed with parentheses, Some(5), and an empty one by its name alone, None.

With Option declared, we can use it to specify the type for some values:

mut anint Option[i32];
mut someint Option[i32];
mut afloat Option[f32];

anint and someint have the same type: a nullable integer. However, afloat's type, a nullable float, is different. Rather than having to declare a new type for every variation, a single generic type declaration supporting type substitution can get the same job done more easily.

Once declared, a generic type may be used anywhere any other declared type may be used, including a generic type as a parameter into another generic type. 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. A generic base is always named with its type arguments, and a generic extension usually passes its own parameters on, as in enum Pending[T] extends Option[T] {Waiting;} (see Extending an enum).

Generic Struct#

A generic struct may be declared in the same way. This generic type defines a resizeable collection:

struct List[T] {
	pub ptr *T;
	pub used usize;
	pub cap usize;
}

And now we can use it:

mut string List[u8];
mut pointers List[So[Node]];

A generic type's functions are reached through an instance, as any type's are through its name:

imm names = List[u8].empty();   // calls List[u8]'s own empty function

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 regular or generic type may define and use generic methods. Generic methods 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
}

imm h = new Holder(1);
imm a = h.pick(6i32);             // T is i32
imm b = h.pick[i64](7);
imm 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).

Value Parameters#

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 {
  mut 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:

imm a = new Array[f32, 3] <- 1.0, 2.0, 3.0;
imm b = new Array[f32, 3] <- 4.0, 5.0, 6.0;
imm x = dot(&a, &b);      // N is 3, read off both arrays
imm y = dot[3](&a, &b);   // the same instance
imm z = filled[i32, 4](7);   // fn filled[T, N usize](v T) Array[T, N]

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 in the same way, its type parameters on its mod line, mod stack[T];, and each use naming an instance, stack[i64].push(3). Each instance is a module of its own, with its own global variables and its own initializer. See Generic modules.

Multiple Types#

A generic type (or function) can substitute more than one type:

enum Result[T,E] {
  pub Ok T;
  pub Err E;
}

Once again, we can use this generic declaration to create specialized result types for different values:

mut i32OrBad Result[i32, BadArith];
int openFile Result[FileHandle, IoError];

Generic Constraints#

Constraints act as a early-warning system, helping to determine whether generic logic can be successfully applied to any given types. For example, a twice generic function adding a value to itself cannot be applied to a type that does not support addition.

Supertypes as constraints#

The simplest form of generic constraint is to specify a Supertype when declaring the generic types:

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 (see 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; and Pointer to the raw pointer types, *T whatever T, and not to a reference; and Sendable to every type whose values may cross to another thread (Race condition safety); and 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 at compile time, 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.

By constraining T, we get a clearer error should we try to apply twice to a type that is not an integer: twice(2.5) is refused where it is written, “twice requires T is Integer, and f32 is not Integer”, with nothing reported from inside twice. 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 (see Traits), 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;}
}

imm low = new Money(250);
imm 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 max above cannot yet be called on two i32 values.

Combining supertypes#

Multiple supertypes may be specified as constraints for a single generic variable using +. This ensures the supertype complies with all of their requirements:

fn fill[T Integer + Copy](dst &mut Array[T], v T) {
  ...
}

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. The constraint 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];
}

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] {...}
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: 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, are exactly those (the parameters' names do not matter, and a parameter may be written as its type alone, fn(&T, &T) i32). 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.

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 spawn[F fn() + Sendable](work &mut F) {...}

fn byName(a &Person, b &Person) i32 {...}
struct Descending { pub fn `()`(self &, a &Person, b &Person) i32 {...} }

sortBy(&mut people, &byName);        // a plain function, referenced, meets it as it is (F is its type)
sortBy(&mut people, &descending);    // so does a struct with that '()'
sortBy(&mut people, (a, b) => a.age - b.age);    // so does a closure written there: its types come from the signature

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
imm 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

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.

Where clause#

The same constraints may instead be written as a where clause just before the start of 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 {
  ...
}

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 So[str], or U is Option[T] (met when U is the Option of whatever T is).

struct Atomic[T] where T is Integer or T is bool or T is Pointer {
  ...
}

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;
  }
}

mut ci = new Cell[i32](3);
mut cb = new Cell[bool](true);
ci.add(4);       // 7
cb.load();       // true
cb.add(true);    // ERROR: no method add for Cell[bool]: it exists only where T is 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. The core package'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 is 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: 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#

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;

This would express the criteria that T is a supertype of Y and that a nullable T is a valid Node. Neither form is built, nor not, nor a constraint on a generic module's or a macro's parameters. 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, as Go holds a generic body to the operations every type in a union constraint has: 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.