[Author’s note: If you’ve read Curtis Poe’s most recent blog post, what follows is going
to seem eerily familiar. This post uses the same concepts, arguments, and code examples as his, and
reaches exactly the same conclusions. That’s because both posts originate from a long
and wide-ranging email discussion between Curtis and myself, as I’ve been privately
consulting with him over the past few months on the design of Corinna.
When I read Curtis’s post I almost decided to bin this one, as he managed to cover everything
necessary in his usual succinct, eloquent, and engaging style. But he has encouraged me to post
my version of this discussion too, as it provides a little more detail on some of the issues
we’re addressing, and on the design rationale for the changes we are jointly proposing.
Personally, I always relish the opportunity to read two versions of exactly the same story by
two very different authors, or to watch two directors’ very different takes on the same
screenplay. In fact, that’s what initially attracted me away from SmallTalk/C++/Eiffel
and into Perl: I read Larry’s version of “Object Orientation”, and found it much more
entertaining, and also more enlightening than the other earlier interpretations.
So here’s the “Conway cut” of our joint proposal.
Of course, if you’re short on time, you should just go and read
Curtis’s original version of this story.
But if you’d like some extra insights into the syntactic design of Corinna (and of Perl itself!),
and perhaps a slightly more detailed and stereoscopic view of the issues we’re addressing...read on!]
The Corinna project is fundamentally about providing
a declarative mechanism for building OO code in Perl,
as opposed to Perl’s current fundamentally emergent approach.
So let’s look at the way we declare things in Perl.
Overwhelmingly, declarations (of variables, subroutines, packages, and formats)
are made using the following syntax:
<keyword> <identifier> <modifiers>? <setup>?
For example:
<keyword> <identifier> <modifiers>? <setup>?
my $lexvar :shared
our $packvar :Tracked = 0
state $statevar :Readonly = 1
sub action :lvalue () {...}
package Namespace v1.2.3 {...}
format Report = ... .
Note that in every case, except for the package
keyword, any modifications
or deviations from standard behaviour (i.e. anything in the <modifiers> column)
are always specified by attributes. And, arguably, the package
version syntax
is a mis-design; it should have been :vers(v1.2.3)
instead.
The only other significant deviation from this general syntactic pattern
is the way in which modern lexical subroutines are specified:
my sub foo () {...}
This syntax concatenates two keywords to denote the non-standard
behaviour of a lexically scoped subroutine. However, I would argue that
this too was a mis-design, or at least an anomaly. It would have been
much more consistent with Perl’s underlying syntactic structure if the
syntax had been:
sub foo :lexical () {...}
Or, perhaps, if we had added a completely new declarator keyword
to reflect the fundamental differences between package and lexical subs:
lexsub foo () {...}
Except in the two cases noted above, the unifying principle of Perl’s
declaration syntax is that the keyword specifies what the declarand is,
and the optional modifiers (i.e. attributes) specify how this particular
declarand differs from the standard behaviour for that kind of declarand.
Or in OO terms: the keyword specifies the standard type
that the declarand IS-A new instance of, and the modifiers
specify any unusual roles/traits or non-standard behaviours
that this particular declarand also DOES.
Which brings us to the new object-oriented declaration syntax
proposed in Corinna.
Here’s a somewhat contrived example that attempts to exercise
all of the new OO features that Corinna provides:
role Tracked {
slot $msg :param;
method report () { $self->show($msg++) }
method show;
}
class Root {
method is_root () { return 1; }
}
abstract class Counter {
my $obj_count = 0; # common slot for all objects in class
method obj_count :common () { return $obj_count; }
ADJUST { $obj_count++ }
DESTRUCT { $obj_count-- }
}
class MetaHandler isa Counter does Tracked {
slot $handler :handles(exists delete) = Handler->new;
slot $size :reader :param = 100;
slot $created :reader = time;
ADJUST { croak("Too small") if $size < 1; }
DESTRUCT { $handler->shutdown; }
method insert :private ($key, $value ) {
if ( ! $self->exists($key) ) {
$handler->set( $key, $value );
}
}
method show ($msg) { say $msg; }
method obj_count :overrides :common () {
$self->next::method() - 42;
}
before method obj_count :common () { warn "Counting..."; }
after method obj_count :common () { warn "...done"; }
around method obj_count :common () { return 1 + $class->$ORIG(); }
}
Note that most of these new constructs conform well to the standard syntactic structure
for declarations:
<keyword> <identifier> <modifiers> <setup>?
role Tracked {...}
class Root {...}
slot $msg :param
slot $handler :handles(exists delete) = Handler->new;
slot $size :reader :param = 100;
slot $created :reader = time;
method is_root () {...}
method show
method report () {...}
method obj_count :common () {...}
method insert :private ($key,$value) {...}
method show ($msg) {...}
method obj_count :overrides :common () {...}
But a few of them diverge significantly from that standard syntactic structure:
<modifier> <keyword> <identifier> <setup>
abstract class Counter {...}
<keyword> <identifier> <keyword-modifiers> <setup>
class MetaHandler isa Counter does Tracked {...}
<modifier> <keyword> <identifier> <modifiers> <setup>
before method obj_count :common () {...}
after method obj_count :common () {...}
around method obj_count :common () {...}
So while Corinna usually specifies modifiers on a declarand in the standard
Perl <modifier> syntactic position, sometimes a modifier is instead
specified via a prefix keyword, or with both a prefix keyword and
something in the usual <modifier> slot:
before method obj_count :common () {...}
Of course, the class
and role
declarators do specify some modifiers
(namely: the classes they inherit and the roles they compose)
in the third syntactic position that is traditionally reserved for modifiers.
But they don’t specify those modifiers in the standard form: as attributes.
Instead, they are specified as internal keyword-modifier subsequences:
class Name isa <modifier> does <modifier> {...}
There is one other significant deviation from standard Perl
syntactic structure in the Corinna proposal: a particularly
nasty case in which specific non-standard behaviour must be
requested implicitly by context, rather than explicitly
by a distinct keyword or attribute.
Namely, when specifying class data slots:
my $obj_count = 0; # common slot for all objects in the class
Of course, this declaration does still have a keyword, but that my
keyword
is silently behaving very differently from every other my
keyword, simply because
of the context in which it’s being used. Hence the need to comment it,
as a reminder of those unusual extra behaviours.
So how is it different? (And notice that the very fact you have to ask about this
tells you that it really shouldn’t be different at all!)
The example my
keyword does still inject the symbol $obj_count
into
the current lexical scope but, because that lexical scope is the block
of a class, that particular my
has two fundamental context-sensitive
differences from any other my
declaration anywhere else in the program.
Specifically, because it is declared inside a class
declaration,
the initialization behaviour and the destruction behaviour of
this lexical variable are completely different. Normally, a my
variable is initialized at run-time, every time it is encountered
in the code. And it is garbage-collected in the usual way
whenever it goes out of scope with a zero reference-count.
But inside a Corinna class, a my
variable is initialized only
once — at compile-time — and is garbage-collected only once — after
all execution terminates.
That’s the correct and necessary behaviour for a variable which is
acting as a class data slot, because we have to be able to use
class data slots everywhere, including inside a BEGIN
, CHECK
, INIT
,
or END
block. So class data has to be initialized
at compile-time and persist until the end of execution.
But those special context-sensitive initialization and destruction
semantics are nothing like the behaviour of a regular my
variable
in any other Perl code. So, even though they’re essential to the correct
working of their surrounding class, those semantics are
also likely to be misleading, confusing, bug-inducing, and brittle.
So, how do we fix these various problems?
The inconsistencies that are purely syntactic in nature:
abstract class Counter {...}
class MetaHandler isa Counter does Tracked {...}
before method obj_count :common () {...}
after method obj_count :common () {...}
around method obj_count :common () {...}
...could easily be handled either by replacing all pre-keyword modifiers
and all post-identifier keyword-modifier pairs with simple attributes:
class Counter :abstract {...}
class MetaHandler :isa(Counter) :does(Tracked) {...}
method obj_count :before :common () {...}
method obj_count :after :common () {...}
method obj_count :around :common () {...}
...or, alternatively, by providing additional keywords to replace the current prefix
modifier-keyword sequences:
abstraction Counter {...}
before obj_count :common () {...}
after obj_count :common () {...}
around obj_count :common () {...}
Creating new attributes is almost certainly the better choice here, however.
One of Corinna’s fundamental design principles is to avoid adding new
declarator keywords to Perl wherever possible. So far, we’ve only needed to add four:
class
, role
, slot
, and method
.
We could add more keywords for abstract classes plus the three kinds of
method wrappers, but that would double the proposed number of new keywords,
and with a proportionately much smaller payback on the second four,
as those specialized constructs are
far less common than classes, roles, slots, and methods.
Method wrappers can also be a “code smell” in OO code, indicating something
is amiss is the original design of a class hierarchy. So it seems odd to
devote three entire keywords to them.
Of course, the concept of being able to extend an existing chunk
of behaviour without having to copy-and-paste the original code is,
in itself, perfectly valid. But that concept is also
not unique to methods. We might, for example want to be
able to wrap regular subroutines as well. But if we make before
mean before method
, we’d then have to come up with yet another
keyword (ante
? ere
?, afore
?) if we ever want to support
before sub
. And that way lies madness.
To leave open the future possibility of general-purpose wrappers,
it’s clearly better to specify method wrappers with attributes
(:before
, :after
, :around
), so that those same
attributes might later be applied to subroutines as well,
if we ever decide that Perl should also have that capacity.
As for the implicit, context-sensitive behaviour of my
inside
a class or role, it could also be made explicit and context-free
via either of those same two syntactic changes.
That is: either by an explicit attribute on an existing keyword:
slot $obj_count :common = 0;
...or else with a more precise, distinct, and explicit keyword:
common $obj_count = 0;
Here too, an attribute is probably the right choice. A class data slot
still intrinsically IS-A data slot, just one that DOES something
slightly different in terms of its initialization and destruction.
So the slot
keyword is the right one for it, with a :common
attribute to denote the differing behaviour.
Moreover, we already specify class methods using the :common
attribute.
So it would be more consistent (i.e. more teachable and more likely to just DWIM)
if we also specify class slots with that same attribute.
But whether or not we add a common
keyword, or simply allow
a :common
attribute on the slot
keyword, the use of my
as a declarator for class data slots really has to go.
Having declarators whose behaviour silently changes in fundamental
ways depending on their context is always a Very Bad Idea.
Note, too, that by adopting the slot $name :common
approach for
specifying class slots, we also remove the very real annoyance
of always having to hand-code any accessors for such slots:
my $obj_count = 0;
method obj_count :common () { return $obj_count; }
...because now we can just write:
slot $obj_count :common :reader = 0;
The only remaining problem is the actual name of the :common
attribute.
While I do agree that “common” is the least-worst linguistic alternative
to have been suggested so far, it’s still significantly less-than-awesome.
So what is the fundamental difference between an instance slot and a class slot,
or between an instance method and a class method?
Well, it’s right there in the definition:
Class slots and methods are mutually shared
and jointly accessible by all objects, classwide.
So, instead of:
slot $obj_count :common = 0;
method obj_count :common () {...}
before obj_count :common () {...}
...why not:
slot $obj_count :joint = 0;
method obj_count :joint () {...}
before obj_count :joint () {...}
...or:
slot $obj_count :mutual = 0;
method obj_count :mutual () {...}
before obj_count :mutual () {...}
...or even:
slot $obj_count :classwide = 0;
method obj_count :classwide () {...}
before obj_count :classwide () {...}
Myself, I rather like :joint
. It’s concise, distinct, and (best of all)
I’m sure there are a couple of really good “role-ing a joint” and
“high-class joint” puns to be had in there somewhere!
Ahem.
Anyway, putting all those ideas together, the earlier full example
would become:
role Tracked {
slot $msg :param;
method report () { $self->show($msg++) }
method show;
}
class Root {
method is_root () { return 1; }
}
class Counter :abstract {
slot $obj_count :common :reader = 0;
ADJUST { $obj_count++ }
DESTRUCT { $obj_count-- }
}
class MetaHandler :isa(Counter) :does(Tracked) {
slot $handler :handles(exists delete) = Handler->new;
slot $size :reader :param = 100;
slot $created :reader = time;
ADJUST { croak("Too small") if $size < 1; }
DESTRUCT { $handler->shutdown; }
method insert :private ($key, $value ) {
if ( ! $self->exists($key) ) {
$handler->set( $key, $value );
}
}
method show ($msg) { say $msg; }
method obj_count :common :overrides () {
$self->next::method() - 42;
}
method obj_count :common :before () { warn "Counting..."; }
method obj_count :common :after () { warn "...done"; }
method obj_count :common :around () { return 1 + $class->$ORIG(); }
}
They’re not huge changes to the current proposal,
but I think they make the new OO syntax cleaner, clearer,
more consistent, and — most importantly — more Perlish.