Skip to content

Instantly share code, notes, and snippets.

@nikomatsakis
Created November 8, 2011 17:47
Show Gist options
  • Save nikomatsakis/1348511 to your computer and use it in GitHub Desktop.
Save nikomatsakis/1348511 to your computer and use it in GitHub Desktop.
"Categories", "object extensions", or just "objects without classes"

Categories

Goals

This proposal is a bridge Haskell's type classes and class-based OOP. It also addresses a pet theme of mine, which is the ability to add methods to classes and define groups of related methods together.

The core unit of organization is called a category, and it is a bundle of statically dispatched methods based on a shared receiver type (a form of static overloading, essentially). Traits can be used as before to implement these methods and provide implementation inheritance. Interfaces can be used as before for polymorphism.

Categories can either complement the classes found in pcwalton's proposal or they can replace them altogether. If one preserves classes, you can have a stronger notion of privacy. Preserving classes also allows for destructors, which don't make sense in a system based purely on categories.

Syntax

I assume a nominal type system, though this is not a requirement. Records are declared using the struct keyword. There is no class keyword. Instead, there is the category keyword that is used to declare a group of methods. It works as follows:

category name(T) {
  fn foo(params) -> ret { /* self has type &T */ }
  priv fn bar(params) -> ret { /* self has type &T */ }
}

This declares a set of methods that can be invoked on any instance of type T, which can be any type. The method bundle has a name (name), but it is not a type. Method bundles can be imported. When a method call rcvr.foo(...) is seen, the type of rcvr is resolved and then all imported method bundles are searched for a function foo(). If one is found, then the call is statically dispatched.

Structs

Although not necessary, I think the system works more smoothly if we move to nominal records, which I have termed struct. The syntax would be:

struct T<...> {
  member1: T1;
  mutable member2: T2;
  priv member3: T3;
}

and so forth.

Access control

Methods and fields may be marked as private. The effect of this is to disallow access to those members except (a) when the instance of the struct is initially created and (b) from methods declared on that struct type.

There have been objections that this notion of private is not very, well, private. This is true. A stronger notion could be achieved by allowing methods to be defined inside of structs, and saying that those methods are the only ones with access to the private fields (in that case, structs are basically classes, because they would combine fields and a default category of methods, as well as a constructor).

Traits

One can incorporate traits in the typical way:

category name(T) : trait1, trait2 { ... }

If an object is cast to an interface, the set of imported methods is searched to find matching objects for each interface method.

Constructors

There is no need of constructors in this system. A constructor is just a function that returns an instance of the struct:

fn make_struct_T(m1: T1, m2: T2) -> T {
   ret {
     m1: m1, m2: m2, m3: initial_value_for_private_field
   };
}

Generics

Method blocks may include generic parameters. For example, the following category

   category<A> vec_mthds([A]) {
     fn len() -> int {
       // self has type &[A]:
       ret vec::len(self);
     }
   }

Regions, boxes, etc

self is always passed by reference and therefore has the type &T where T is the type of the method receiver. A function name can be prefixed by @ to require that self has the type @T.

Potential abuse

Because multiple blocks of methods can be defined for any given type, there is the chance for ambiguity and odd scenarios. Consider:

  struct T { ... }
  category foo1(T) { fn bar() { ... } }
  category foo2(T) { fn bar() { ... } }
  iface inter { fn bar(); }

This raises several questions:

  • What happens when t.bar() is invoked if both groups of methods are in scope?

    • I think the result is a static error. Perhaps we allow the syntax t.foo1::bar() to make it clear.
  • What happens when t is cast to an instance of inter?

    • Again a static error.

Finally, this also raises the potential to have two instances of the interace inter, both based on the same receiver, but with different vtables and hence different definitions of bar()! This can arise if you have two separate modules like so:

Module 1:
import T, inter;
category foo1(T) { fn bar() { ... } }
fn make_i(t: T) -> i {
   ret t as inter; // uses foo1::bar()
}

Module 2:
import T, inter;
category foo2(T) { fn bar() { ... } }
fn make_i(t: T) -> i {
   ret t as inter; // uses foo2::bar()
}

I see this potential for abuse as a fair trade for the power of defining methods on any type and also breaking them up into categories and so forth. Others may disagree.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment