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.
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.
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.
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).
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.
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
};
}
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);
}
}
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
.
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.
- I think the result is a static error. Perhaps we allow the
syntax
-
What happens when
t
is cast to an instance ofinter
?- 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.