Cone Language Reference

Delegated Inheritance

On this page

Inheritance makes it easy to declare a new type that reuses (inherits) the fields or methods defined by some other type.

Cone's version of inheritance looks and behaves somewhat differently than in other languages, which is why it is called delegated inheritance. At its heart, Cone's inheritance is a composition-based approach that offers convenient namespace "sugar" for automatically forwarding certain method (or field) access requests. This makes method reuse (forwarding) easy to accomplish, where needed, while side-stepping the tightly-coupled complexity and fragility sometimes experienced when using traditional inheritance.

It is the same operation a module performs when it folds another module's names into its own: "the names in there are now names in here." A type can do it in three places, and they answer different needs. On a field it delegates to a part, which is the whole of this page until its last sections. On the type itself, with extends, it starts from a concrete type and enriches it with methods — see Enriching a concrete type. And in the type's body, with use, it folds in a sibling: somebody else's enrichment of that same base — see Composing several enrichments of one base. A type that lends a body — a String lends the text it holds as a &str — folds the body's methods in with the same use and a via: see Folding a body the type lends.

There is a fourth place, and it is not inside a type at all: a module's global variable may carry the same clause, folding its type's members in as names of the module — see Folding through a global. A module has exactly one instance, so that is the field's fold with the receiver settled in advance.

And a fifth, which reaches through nothing: a module's use statement naming an enum folds the enum's variants in as names of the module, use Colors;. It admits variants and nothing else of the enum, and what follows the enum is the clause a type body's use has — see Naming a variant.

And a module may be built out of another module the way a type is enriched from a base: mod solids extends shapes; makes everything shapes declares or folds in a name of solids, public where shapes shows it and private where it keeps it, just as an enriching type reads its base's private members. For a module, extending and inheriting are one thing, because a module has only one instance to share — see A module extending a module.

Selective Inheritance#

Imagine we have this Engine type:

struct Engine {
  pub fuel   f32;
  pub torque f32;

  pub fn thrust(self &mut, amt f32) { ... }
  pub fn power(self &) f32 { ... }
}

Now, let's create a new Car type that includes, and inherits from, an Engine:

struct Car {
  pub engine Engine use fuel, thrust;
  pub body   SportyLook;
  pub wheels RimWheels;
}

The use clause on the engine field makes the fuel field and the thrust method of Engine names of Car, reached by way of engine:

  • any access to car.fuel is treated as equivalent to car.engine.fuel. It reads and writes the engine's own field; no second copy of the value exists.
  • (&mut car).thrust(23.2) is treated as equivalent to (&mut car.engine).thrust(23.2). It is important to note that thrust runs with self being the engine, and not the car. If one needs a thrust method that operates on the car, then don't have Car inherit it from engine. Implement it in Car and then explicitly call engine's thrust as part of its logic, if desired.

No forwarding method is generated for a folded name: a call to thrust through a Car is a call to Engine's own method, with its receiver shifted to the engine. Inside Car's own methods a folded name is named bare, exactly as Car's own members are.

A type may inherit names from more than one field. One clause reaches one level: use temp admits engine.temp, not engine.core.temp, unless Engine itself folded temp in from core. Folds chain through the types, not through the clause.

Name Aliasing#

Inherited names must be unique. It is an error to inherit names that are otherwise defined by the type. Similarly, it is an error to inherit the same name from two different fields.

To avoid such name conflicts, a name may be inherited using an alias. For example use fuel as fuelamt, thrust would support the use of car.fuelamt as an alias for car.engine.fuel.

Total Inheritance#

If a field specifies use *, every public field and method of the field's type is inherited, less any named after but. A name after but must be a public member of that type: one it does not have is an error, and so is a private one, which * never folds, so there is nothing to leave out. The rule for names is the same as for a listed fold: an inherited name may not collide with a name the type declares, so a type that implements a method of its own leaves the inherited one out by name. For example:

struct SuperEngine {
  pub engine Engine use * but power;
  pub fn power(self &) f32 { ... }
}

SuperEngine inherits the fields fuel and torque, as well as the thrust method from engine. It does not inherit the power method, since it has its own implementation of it and says so. Should Engine later gain a member whose name SuperEngine also declares, the fold is refused where it is written rather than one quietly overriding the other.

What folds, and what does not#

  • The field must be pub, and only public members fold. Visibility is transitive: a folded name is reached through the field, so a private field cannot lend its members out, and a private member of the field's type stays private. A folded name is public because its path is.
  • Fields, methods and macro methods fold. A static function or variable of the field's type does not. A field is the route to something each value holds, and a static is one thing for the whole type, never reached through a value, so it has no business arriving through one; it folds only on a type, by extends or a sibling use, and is otherwise named through its type. Neither does a macro without a self parameter, nor Self, nor the field type's own final or clone.
  • A field held through a reference folds. With pub engine &mut Engine use fuel, thrust, the hop reaches through the reference as car.engine.fuel does, and the reference itself is the receiver of a folded method.
  • Only a struct folds. A trait and an enum are abstractions over their implementers, with no organizing details of their own to fold through; a use clause on one of their fields is refused.
  • A fold needs the field's type complete. A type cannot fold from a field of its own type, nor may two types each fold from the other.

Receivers#

A folded method is reached exactly as the path written out would reach it. Through a reference to the folding type, the field is reborrowed with the reference's own permission, so (&mut car).thrust(1.0) and c.thrust(1.0) for a c &mut Car both hand thrust a &mut Engine. From a value of the folding type, the receiver is the field's value: a method that takes self by value copies the field, as car.engine.rating() would, and one that declares self &mut borrows it from a mutable car, so car.thrust(1.0) is car.engine.thrust(1.0) (see Implicit Coercion of 'self'). The fold adds no borrowing rule of its own.

What a fold grants#

A fold grants names, and with them structural compliance with any trait those names satisfy: a type whose folded power matches a trait's requirement conforms to that trait, and may be reached through a virtual reference to it. Behind the virtual reference the compiler supplies the shift to the field, since the call site cannot know the type folded the method; nothing in the language can name what it supplies.

A fold grants no subtyping to the field's concrete type, and no layout: a Car is not an Engine, and &Car does not recast to &Engine. Subtyping in Cone is between a concrete type and an abstraction, a trait, never between two concrete types.

A folded field is not a field of the type. It is a name for storage inside another type's value, so it cannot be named in the type's construction (new SuperEngine(engine: new Engine(fuel: 1.0, torque: 2.0)) is the construction), and it meets no trait's field requirement. A folded method does count toward conformance.

Enriching a concrete type#

Sometimes what you want is not a part but a better version of a whole. You have somebody's floating-point type, or matrix type, or parser state, and you want it plus trigonometry, plus logarithms, as if they had always been one type. You do not reach into their package and add methods to it. You declare a type of your own that names theirs as its base:

struct Meter {
  pub mut cm i32;
  scale     i32;

  pub fn scaled(self) i32 { cm * scale; }
  pub fn make() Meter { new Meter(4, 5); }
}

struct Gauge extends Meter {
  pub fn doubled(self) i32 { cm + cm; }
  pub fn reset(self &mut) { cm = 0; }
}

Gauge adds methods and no fields. Everything Meter has is a name of Gauge — its fields, its methods, its static functions — and Gauge's own methods read and write those fields by bare name, as doubled and reset do. What it may not do is declare a field. Reading and writing the fields it starts with is exactly what it is for; adding one is what would make the two types different shapes.

The two types interchange, in both directions, at no cost#

Because the representations are identical, a Gauge may be used wherever a Meter is wanted, and a Meter wherever a Gauge is wanted — by value, through a & reference, and through a &mut reference. No conversion runs; there is nothing to convert.

fn wantsMeter(m Meter) i32 { m.scaled(); }

imm g Gauge = Meter.make();   // a Meter value, bound to the enriched name
wantsMeter(g);                // and a Gauge, passed as a Meter

That last pair is how values of an enriched type are obtained at all. The base's own constructors and static functions still return the base type — Gauge.make() hands back a Meter — and binding the result to the enriched type is all that is needed. A library that never heard of your enrichment can still produce values for it.

They remain two distinct types, and which one you name is what decides what is in view. A value typed as Meter cannot call doubled; you reach the added methods by naming Gauge somewhere. That is the whole difference between this and a language where extension methods drift into scope with an import: an enrichment here is owned, and it is reached deliberately.

The declaration is what licenses it#

Two types interchange because one of them said the other was its base, not because they happen to look alike. Identical representation is why the license is safe to grant; the declaration is why it is granted at all.

  • Two look-alikes that named no base stay unrelated. A struct with the same fields in the same order as Meter is not a Meter and does not substitute for one. Cone has no structural typing between concrete types.
  • Nothing lifts through a container. Pair[Gauge] and Pair[Meter] are two instantiations of one generic, neither of which extends anything, so they are unrelated however their components are declared. Substitute at the element, never at the container.
  • A type enriches one base, and a chain is allowed: Fancy extends Gauge extends Meter. Every hop adds methods only, so all three are one representation and any of them substitutes for any other. Siblings interchange too: two types that both extend Meter both have its representation.
  • An enrichment may not override. A name the base already has may not be redeclared, whether the base's is a field or a method. One value would otherwise mean two different things depending on which of the two names was used to reach it.
  • Only a concrete struct may be enriched. An abstraction holds no value, so there is nothing of it to enrich — what a type asserts about an abstraction is conformance, which is is, and a type may write both clauses. An enum's variant set is its identity, so adding to an enum means adding variants, which is a separate relationship (see enum).

A finalizing base finalizes its enrichments too#

A base may declare a finalizer, and enriching it changes nothing about how its values are destroyed:

struct File {
  pub fd i32;
  fn final(self &uni) { close(fd); }
}

struct LoggedFile extends File {
  pub fn describe(self &) i32 { fd; }
}

imm f LoggedFile = new File(open());   // closed once, at the end of f's life

LoggedFile has its own copy of File's final, running on a LoggedFile, so a value is finalized by the same logic whichever of the two names holds it — and, since a finalizer makes a type move rather than copy, a LoggedFile moves too. Where the base also holds fields that are finalized, the finalizer runs first and then theirs, exactly as for the base. A clone method comes across the same way, and one that builds a Self builds the enrichment.

Two consequences of rules already stated. The enrichment may not write a final of its own over the base's, because that is overriding a name the base has. And it may not add a field, even one that owns a resource, so what its values have to release is exactly what the base's do.

What the base declares about its values comes across the same way. An enrichment of a type declaring is Move moves, and an enrichment of an @opaque type is opaque: the declaration is written on the base, but what it describes is the one representation both names share. So an enrichment of a type that moves may not declare is Copy.

An enrichment is inside its base's encapsulation#

An enrichment is acting as the base, so it sees what the base's own methods see. Above, scale is private to Meter's module and Gauge's methods may read and write it — even when Meter lives in another module or another package. What comes across keeps the base's visibility, so it is private to Gauge's module, and Gauge's clients outside it cannot reach scale at all. The asymmetry with a field's fold is deliberate: a type that folds through a field is a client of the part and sees only what is public, while an enrichment is the whole.

A type that may be extended has its representation in its contract. This is the price of the paragraph above, and it is worth stating plainly because nothing in the language can warn you about it. extends is an intimate relationship, and its author takes the coupling knowingly. Adding a field to a base changes the size and shifts the positional literal, so it is a major version change and never a quiet one. But so is renaming or retyping a private field at the same size — invisible to everyone except an enrichment reading it, and the base's author has every reason to think private means free to change. If you publish a type others may extend, treat its field names and types as part of what you promised. Semantic versioning is the protection, and it works because two versions of one package may coexist in a binary.

Retroactive conformance#

Cone notices trait conformance structurally: a type that happens to have the methods a trait requires may be passed where that trait is wanted, without declaring anything (virtual references). Put that together with the enrichment above and something useful falls out. Suppose a library's type implements three of a trait's four methods:

trait Area { pub fn area(self &) i32; }

struct Framed extends Meter {
  pub fn area(self &) i32 { cm * cm; }
}

fn areaOf(a &<Area) i32 { a.area(); }

imm f Framed = Meter.make();   // a Meter value
areaOf(&f);                    // reaching a trait Meter does not satisfy

You supply the fourth method in an enrichment of your own, and the library's own values reach the trait by being bound to your name. You did not modify their package and you did not need their permission. And because the conformance hangs off your named type and its own vtable, two people can enrich the same base in conflicting ways without colliding: each one's claim is reached only through the name that made it.

Generic types#

A generic may enrich a generic — struct Rich[T] extends Holder[T] — and the license sits on that one declaration rather than on each instantiation, because no field may be added and so the shapes agree instance by instance. Inside such an enrichment, a member taken from the base is reached as self.name rather than by bare name, because the base is not a type yet when the enrichment's bodies are read. This is the same limit an inherited member of a generic trait has.

Composing several enrichments of one base#

The motivating case for enrichment was never one library. You have somebody's floating-point type. One package has already enriched it with trigonometry. Another has enriched it with logarithms. Neither knows about the other, and you want all three at once, plus a method or two of your own. So you enrich that same base and use their enrichments in your body:

struct Trig extends Meter {
  pub fn sine(self) i32 { cm * scale; }
  pub fn span(self) i32 { cm * 2; }
  pub fn tangent(self) i32 { cm * 3; }
}

struct LogExp extends Meter {
  pub fn logOf(self) i32 { cm + scale; }
  pub fn span(self) i32 { cm * 4; }
}

struct Rich extends Meter {
  use Trig but tangent, span;
  use LogExp logOf, span as wide;

  pub fn both(self) i32 { sine() + logOf(); }
}

A type named by a use here is a sibling: it declared the same base this type declares. That is the whole rule, and it is what makes the fold sound — a sibling's method takes a receiver of the sibling's type, and values of a type that shares the base substitute for that type, so sine may run on a Rich exactly as it runs on a Trig. Nothing is copied, nothing is forwarded, and no wrapper is generated.

One declaration, in your namespace, and neither library was touched. Naming Rich is what puts all of it in view, and values still come from the base the ordinary way: imm r Rich = Meter.make();

Selective, and renaming#

A bare use Trig takes every member the sibling adds. Naming the sibling is what asks for its members, so there is no * to write here — and the two ways to narrow it are the same two a field's clause has:

  • use Trig but tangent, span — everything except those.
  • use LogExp logOf, span as wide — only those, the second under a spelling of your own.

Which is why the example needs both. Trig and LogExp each declare span, and neither is wrong to. A collision between two siblings is reported at your declaration, and your declaration is where it is settled — leave one out, or rename it — without asking either package to change anything. That is the case this construct exists for.

What folds from a sibling#

Only what the sibling adds. Everything it took from the base is already yours by the same route, so it does not arrive twice: use Trig cm and use Trig scaled are both errors, and a bare use Trig passes them over rather than colliding on every one. What does fold is the sibling's own methods, its overload sets, its macro methods and its static functions and variables. A static keeps its owner and its signature, exactly as a base's does — a static folded from Trig that returns a Meter still returns a Meter.

The other limits:

  • A sibling may not add fields, and cannot: it is an enrichment, so it has none of its own. Every type sharing a base has one representation, which is what the whole construct rests on. Folding a type that could add fields is a restriction for now, not a wall.
  • Only a type with a base has siblings. A use in a type that declares no extends is refused, and so is one naming a type whose base is a different one: its methods would read fields you have not got. To reuse an unrelated type, declare a field of it and fold through the field.
  • Only extends takes siblings. is names abstractions, which hold no value for a folded name to be reached through, and it has no use for them.
  • The base itself is not a sibling of yours, nor is any type along the chain down to it. extends has brought all of it already.
  • A sibling's private members stay its own. You and it are both inside the base's encapsulation, which is why its methods may read the base's scale on your value. Neither of you is inside the other, so only what the sibling declares pub folds, and naming a private member is an error, whether in a list or after but. And final and clone belong to its own values' lifecycle and never fold.

A use inside a type is always this — composition, on the producer's side, saying what this type is built out of. It is never an import: ordinary name lookup inside a type's methods comes from the enclosing module, as it always did.

Folding a body the type lends#

Some types hold a thing and show the world a view of it. A String holds a block of bytes, and lends them as a &str, a borrow of text. Everything one can ask of text — its length, whether it starts with something, where something is in it, a part of it — is a method of str, written once. A type that lends a body can make those methods its own names, without writing any of them, by saying which of its methods does the lending:

struct Title {
  text So[str];

  // The borrow of text this type lends
  pub fn view(self &) &str { text; }

  // Every public method of str is now a name of Title, reached through view()
  use str via view;
}

imm t = new Title("Dune");     // a literal is copied into the So[str]
t.len                          // 4:      t.view().len
t.startsWith("Du")             // true:   t.view().startsWith("Du")
&t[1..<3]                       // "un":   t.view().slice(1, 3), a &str of the bytes 1 and 2
t < "Dunes"                    // true:   t.view() < "Dunes", compared byte by byte

It is the field's fold with the receiver rewritten differently. A field's fold reaches a part of the value, car.engine; this one reaches what a method gives, title.view(), so no field is involved and no storage is added. As with a field, no forwarding method is generated: t.len is a call of str's own len, with the borrow view gave as its self, and the same method serves every type that lends a str. The borrow of the call is the view's: while a result of t.trim() is used, t stays borrowed, as it does for t.view() itself.

  • The clause is a sibling's with a via. use str via view; admits every public member; a list after it admits just those, and but leaves some out, as everywhere (use str via view but len, find;). A name the type declares itself is the type's own, and folding the same name is a collision, so the type leaves it out by but and keeps its own. String does this for the few that take a &Array[u8] or answer a copy.
  • The lending method is the type's own — one the type declares, taking the type as self and giving the borrow. via naming nothing the type declares, or a field, is an error.
  • Only methods fold, and only a struct is lent: a field of the body is not reached through a borrow, a static has no value to lend, and an enum or a trait has no body.
  • Every holder reaches them. A value, a borrow, a So[Title] or an Rc[Title] calls a folded method as it calls the type's own, because every owner already lends its value as a borrow to a method's self (see Implicit Coercion of 'self'). Nothing is written per region or per permission.
  • Operators come with them, and compare the lent text. == and the orderings between two kinds of text — a literal, a String, a So[str], an Rc[str] — compare their bytes, whichever are which, so title == "Dune" and "Dune" < title both work; a type that lends text is replaced by its borrow on either side. === is still whether two references are the same place.
  • Ranges are the lent text's. &t[a..<b], &t[a..] and &t[a..b] call slice, sliceFrom and sliceThrough, which refuse a bound that falls inside a character.
  • Where a literal is wanted as the type, a static function fromLiteral(text &str) the type declares makes one from it (imm t Title = "Dune";, or "Dune" passed where a Title is a parameter); that is how a literal becomes a String wherever one is wanted. Where a read-only borrow of the type is wanted (&Title), the literal is lent as a temporary Title.fromLiteral("Dune") that lasts to the end of the statement, or of the block when a local's initializer holds it; a borrow that writes (&mut Title) is refused. The same holds for a parameter's default, t &Title = "Dune": each call that omits the argument makes its own temporary. Where another candidate takes the literal as the &str it is, that one is chosen, and nothing is made.
  • What it does not do: a lend grants names and no subtyping (a Title is not a str, and is passed where a &str is wanted as t.view()), and a folded method fills a trait's slot, so is reached through a virtual reference to the trait, only if it is not inline: the slot points at a small function that lends the view and calls the body's method on it, and an inline method has no function to point at.

The body of a run-time length, Array[T], is lent the same way. A List[T] lends the &Array[T] that its view() gives, and folds the methods of Array[T] in with use Array[T] via view;: its len is Array[T]'s, written once, which every owner of an Array[T] reaches too (owned arrays).

Folding through a global#

A module is a namespace too, and a global variable is to a module what a field is to a type: the one difference is that there is exactly one of it. So a global carries the same clause, and it folds its type's members in as names of the module:

struct Config {
  pub width  i64;
  pub height i64;

  pub fn area(self) i64 { width * height; }
}

mut config Config = new Config(80i64, 24i64) use *;

fn banner() i64 {
  area();          // config.area()
}

width, height and area are now names of this module, reached through config. Every form the field's clause takes is here: use width, area as size lists and renames, use * but height leaves one out, and a long list may be written as a block, use { width, height, area as size }. What folds and what does not is the field's rule, unchanged: only public members, no static, no final or clone, and the source must be a struct rather than a trait or an enum.

What this buys that folding one module into another cannot is composing a module out of a struct: the module presents a singleton's interface as its own names, and a caller writing area() need not know there is a Config behind it. It costs nothing at run time either. A folded name is a name for one thing at one address, known when the program is compiled, so there is no receiver to find and nothing extra is generated — the call is the one you would have written by hand.

What the module shows of it#

A fold is private to the module that made it, whatever the member's visibility in its own type. That is the same rule a folded name has everywhere: the name is this module's, so this module says who may see it. Writing pub before the clause's use opts in, first as it is on every declaration:

pub mut config Config = new Config(80i64, 24i64) pub use width, area as size;

Now another module may write display.width and display.size(). The global itself must be pub for that, since it is what the folded name is reached through. Folding never widens visibility past the origin either: a member that is private in Config does not fold at all, with or without pub.

An extern global carries the clause too, since the fold reads only the global's type, which an extern global declares. That is how a package's include file passes on what a folded global puts into the package: it declares the global extern, with no value, and repeats the source's clause:

pub extern mut config Config pub use width, area as size;

A program compiled against the include file then reaches display.width, or width where it folds the package in, exactly as it would against the package's source; each is the package's one config. Nothing checks that a hand-written include file folds what the source does.

The same clause, pub use included, folds another module's names in when it is written on an import: see Namespace folding.