Trait composition is one of the fundamental ideas in ThiNG r3/r4. It’s used to assemble pattern-matching clauses in functions, to assemble definitions into a module, to (functionally-) update data-structure, to augment generic functions with new definitions, and to assemble modules into programs.
define cons [+car: [+cdr: [First: car Rest: cdr]]] define map [+f: loop=[(cons +a +d): (cons (f a) (loop d)) Nil:Nil]]
Both these definitions use objects with more than one clause - for
[First: car Rest: cdr] contains two clauses, as
does the object after the equal-sign in
map. Multi-clause objects are defined as being
constructed from single clauses using the trait composition operators
sum (+), override (/), remove (-) and rename (@).
If we stick to an imagined syntax where only single-clause objects and trait composition operators can be used, then the above definitions might look like this:
define cons [+car: [+cdr: ([First: car] + [Rest: cdr])]] define map [+f: loop=([(cons +a +d): (cons (f a) (loop d))] + [Nil:Nil])]
Note that sum (+) is used instead of override (/), since the
+a +d) vs
Nil don’t overlap. We could have used
override, but we would have forgone the checks made for overlapping
patterns that the sum operator provides.
Module definitions and generic functions in ThiNG work similarly. There are two dialects of ThiNG I’ve been thinking about, one where a generic function dispatches each message, and one more traditional dialect where messages are sent directly to explicitly-given receivers. Let’s call these dialects ThiNG/1 and ThiNG/N, for single-distinguished-receiver and multiple-dispatch (N receivers) respectively.
In ThiNG/N, you can think of each definition within a module as
being a clause within a (part of a) global generic function. Here’s a
hypothetical example, recalling that
x y: z is syntactic
sugar for the message
[0: x y: z]:
module List = [ [+a cons: +b]: [First: a Rest: b] [+f mapOver: (+head cons: +tail)]: ((f head) cons: (f mapOver: tail)) [+f mapOver: Nil]: Nil ]
(As you can see, this leads to quite a different style of programming compared to ThiNG/1.) Rewriting this using single-clause-with-trait-composition syntax gives something like:
module List = [ ([0: +a] + [cons: +b]): ([First: a] + [Rest: b]) ] + [ ([0: +f] + [mapOver: (+head cons: +tail)]): ((f head) cons: (f mapOver: tail)) ] + [ ([0: +f] + [mapOver: Nil]): Nil ]
Importing a module into the current toplevel is a matter of
replacing the current generic-function-dispatching object - let’s call
ENV - with itself either summed or overridden by the
imported module definition, i.e.
List + ENV or
List / ENV.
I do have a feeling that the direction [toward ultra-simplicity that Self takes], while very fruitful, turned out to be “too uniform”, in some senses. There is no fence at all to the meta-level at one point ( object vs. class ) and still a big one at another ( object vs. code/method ).
In ThiNG, the fence he talks about between objects and methods is removed - or at least made much smaller. Objects are simultaneously data-structures and methods/functions. If an object has a clause keyed on a pattern containing a binding, then it acts more like a method than a field; if the pattern doesn’t contain any bindings, it acts more like a field than a method. Lazy evaluation helps blur the distinction here.