[rust-dev] Object system redesign

Patrick Walton pwalton at mozilla.com
Tue Nov 8 08:18:33 PST 2011

Hi everyone,

I've been working on this for quite a while, and I think it's time to 
get it out in the open. I tried hard to keep this as minimal as 
possible, while addressing the very real issues in expressivity and 
developer ergonomics that we have run into with the current Rust object 
system. It also tries to preserve the benefits of our current object 
system (in particular, the convenience of interfaces).

This has been through several iterations by now, but it should still be 
considered a strawman proposal. Feedback is welcome.


Object system redesign


The current object system in Rust suffers from these limitations:

1. All method calls are virtual.

2. All objects must be stored on the heap.

3. There are no private methods.

4. **self** is not a first-class value, which means in particular that
self-dispatch requires special machinery in the compiler.

5. There are no public fields.

6. Only single inheritance is supported through object extension.

7. Object extension requires the construction of forwarding and
backwarding vtables.

8. Object types are structural and so cannot be recursive.

Additionally, it's a very common pattern to have a context record
threaded throughout a module (most notably `trans.rs`). It would be
convenient if the language provided a way to avoid having to directly
thread common state through many functions.


There are three main components to this proposal: *classes*,
*interfaces*, and *traits*. We describe each separately.

### Classes

A class is a nominal type that groups together related methods and
fields.  A class declaration looks like this:

     class cat {
         priv {
             let mutable x : int;
             fn meow() { log_err "Meow"; }

         let y : int;

         new(in_x : int, in_y : int) { x = in_x; self.y = in_y; }

         fn speak() { meow(); }

         fn eat() { ... }

And its use looks like this:

     let c : cat = cat(1, 2);

Class instances may be allocated in any region: on the stack, in the
exchange heap, or on the task heap.

Fields in a class are immutable unless declared with the **mutable**
keyword.  They may, however, be mutated in the constructor.

Fields and methods of the current instance may, but need not be,
prefixed with `self.`.

Fields and methods in the **priv** section are private to the class
(class-private, not instance-private). Fields and methods in the
**pub** section (as well as fields and methods outside any section)
are public.

The **new** keyword delimits the constructor inside the class
declaration.  It is not used to create instances of the class; rather,
the class declaration results in the introduction of the constructor
into the module as a first-class function with bare function type and
the same name as the class itself.

The constructor must initialize all fields of the object and cannot
call any methods on **self** until it has done so. After calling a
method on **self**, the constructor is not allowed to mutate any of
its immutable fields. The flow analysis used in typestate enforces
these invariants.

There is an alternate class form, **@class** (for example, `@class dog
{ ... }`), which has two effects:

1. An instance of a class declared with **@class** is always allocated
on the task heap.

2. Within a class declared with **class**, **self** is not a
first-class value.  It may only be used to reference fields and
methods. But in a class declared with **@class**, **self** is a
first-class value.

Classes do not feature polymorphism or inheritance except through
interfaces and traits. As a result, all method dispatch is

A class may have a destructor notated by **drop**:

     class file {
         let fd : int;
         drop { os::close(fd); }

Destructors may not reference any data in the task heap, in order to
disallow object resurrection.

Classes with destructors replace resources.

Class instances are copyable if and only if the class has no
destructor and all of its fields are copyable. Class instances are
sendable if and only if all of its fields are sendable and the class
was not declared with **@class**.

The order of fields in a class instance is significant; its runtime
representation is the same as that of a record with identical fields
laid out in the same order.

Classes may be type-parametric. Methods may not be type-parametric.

### Interfaces

Interfaces are the way we achieve polymorphism. An interface is a
nominal type. (This makes recursive types easier to deal with.) The 
following is an example of an interface:

     iface animal {
         fn speak();
         fn play();

Interfaces allow us to create *views* of a class that expose only the
subset of the methods defined in the interface:

     class cat { fn speak() { ... } fn play() { ... } }
     class dog { fn speak() { ... } fn play() { ... } }

     let c : @cat = @cat();
     let ac : animal = c as animal;
     let d : @dog = @dog();
     let ad : animal = d as animal;
     let animals = [ ac, ad ];
     vec::each(animals, { |a| a.speak(); });

Views are represented at runtime as a pointer to the class instance
and a vtable. The methods in the vtable are laid out in memory in the 
same order they were defined in the interface. The class instance must 
be in the task heap; thus all views have shared kind.

Views are created with the **as** operator. The left-hand side of the
**as** operator must be one of (a) a boxed instance of a class
declared with **class**; (b) an unboxed instance of a class declared
with **@class**; or (c) another view. In all cases, the class instance
or view on the left-hand side of the **as** operator must expose, for
each method named in the view, a method whose name and signature

All calls through a view are dispatched through a vtable.

### Traits

Traits are the way we achieve implementation reuse. A trait is not a
type but instead provides reusable implementations that can be mixed
in to classes.

Here is an example of a trait and its use:

     trait playful {
         req {
             let mutable is_tired : bool;
             fn fetch();

         fn play() {
             if !is_tired { fetch(); }

     class dog : playful {
         let mutable is_tired : bool;
         fn fetch() { ... }

A trait describes the fields and methods that the class inheriting
from it must expose via a **req** block. The trait is allowed to
reference those (and only those) fields and methods of the class.

Trait definitions are duplicated at compile time for each class that
inherits from them. In particular, there is no virtual dispatch when
calling a method of the class, so there is no runtime abstraction
penalty for factoring out common functionality into a trait. Note that
this means that a crate exposing a trait must be statically linked
with the crates that implement the trait.

Traits may be combined together. The resulting class inherits all the
methods of all the traits it derives from. For instance:

     trait hungry {
         req {
             let mutable is_hungry : bool;
         fn feed() { ... }

     class dog : playful, hungry { ... }

     let d = dog();

It is a compile-time error to attempt to derive from two traits that
define a method with the same name.

Trait composition affords renaming of fields or methods:

     class dog : playful with play = please_play,
                              hungry with eat = please_eat {

     let d = dog();

Fields or methods named in the **req** section can be renamed in the
same way:

     class dog : playful with is_tired = is_sleepy {
         let mutable is_sleepy : bool;

Traits may not define fields. This prevents the diamond inheritance
problem of C++.

Trait implementations may call private methods and reference private
fields of the class. The methods and fields named in the **req**
section may be either public or private.

The order of trait composition is not significant.

### Scoped object extensions (concepts, categories)

Niko has a proposal for this, so I'll let him present it.


More information about the Rust-dev mailing list