A dream resyntaxed
.QUOTE {
color: Black;
}
[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.
The class data part seems to me to be going the wrong way - I agree that classes should behave *consistently* across phases but I disagree that begin lifting additional bits of syntax is the way to achieve that - while it makes complete sense for named subs to be begin lifted for scripting, there's no reason why the method keyword needs to be - instead *don't* begin lift that, and the code inside class blocks can run in the obvious, visible order rather than the user having to memorise special cases. That also means that the need for class specific class data syntax dissolves, and only 'slot' remains as needing to initialise specially, which is I suspect basically inevitable if we want to be able to use '=' for that, which seems like it'll be more comfortable for users and therefore pay for its specialness.
Note that this has been discussed at some length by various other contributors already both in github issues and on the IRC channel and it's a bit of a shame none of us knew there was also a back channel discussion going on that might suddenly obviate the previous consensus - so to avoid that happening again, here's a link to my first comment in the current discussion thread about class data.
I'd very much appreciate if anything anybody intends to be canonical goes there as well (though if people with blogs.perl.org accounts who find github's ToS unacceptable comment only here, I'm sure we can add a link to those comments from the GH issue to keep everything together)
The only part of the proposal that feels a bit weird for me is `before method foo () {...}` turning into `method foo :before () {...}`.
Hi Matt. Thanks so much for your thoughtful and detailed response, and especially for the pointer to previous discussions on this subject.
And I agree that, if methods weren't BEGIN-lifted, my would be (technically) sufficient for class data.
But can we really specify unlifted methods?
(And is "technically sufficient" really good enough?)
Are people really going to be happy that they can't use objects in BEGIN blocks (or pass them as arguments to use statements)? Or can only use them in those contexts if the object's classes are themselves specified in BEGIN blocks or use-d from a module?
And reusing my for class data also doesn't solve the problem of having to manually code every damn accessor for every damn class data slot. Unless, of course, we allow my to take :reader, :writer, and :predicate attributes. Except, that introduces a special-case too: allowed only immediately inside a class or role block.
But none of these points is the biggest argument against reusing my for class data. To me, against reusing my for class data is precisely the fundamental argument that I'm hearing for using it: that reusing my avoids multiplying entities without necessity.
That's the same argument that originally gave us package for classes and sub for methods. And those well-intentioned design errors are precisely what we're now trying to undo. Or rather: have been trying to undo for decades, through the dozens of class-framework modules we've all created on CPAN.
Using my for class data would be the same mistake. The same mistake that has long made it impossible to write reliable automatic code analysers, or refactorers, or even just accurate syntax colourers for OO Perl. Because software like PPI or PPR, which would be the basis of those tools, simply won't be able to tell whether a particular my is a class data slot, or just an ordinary lexical variable (perhaps, for example, acting as private shared memory for two closures within the class).
And that's my biggest issue here. Using my for class slots lacks design intentionality. If we use my for class data, then the declaration keyword won't reflect the purpose of the variable being declared, and won't distinguish that variable from every other mundane use of a lexical variable in the same class scope. So automated tools won't be able to reliably distinguish class data variables either. And nor will harried developers in the throes of an urgent bug-fix.
I truly don't understand it. We already have the slot keyword. Why this insistence on a minimalist low-intentionality reuse of my, when allowing a :common attribute on slot gives us better distinguishability, better automatic detectability, and better intentionality?
And, even if we decide that slot :common data variables are not going to be hoisted to compile-time (as my presumably won't be...and certainly shouldn't be!), why not declare class data slots with a keyword that clearly labels them as slots, and which also allows us to declaratively add accessors for them as well?
Why be implicit and procedural and clever,
when we could be explicit and declarative and clear?
Thanks for the feedback, Jakub.
And, I agree. The attribute versions don't read as naturally.
But that's not because the attribute version is "weird"; it's because the attribute names haven't (yet!) been adjusted to reflect the changed location of the modifiers.
You are completely right that:
method foo :before () {...}
method foo :after () {...}
method foo :around () {...}
...all feel slightly off.
They should probably be something like:
method foo :preface () {...}
method foo :epilog () {...}
method foo :wrapper () {...}
...instead.
That is: "This is method foo's preface", "This is a method foo wrapper", etc.
"The fault, dear Brutus, is not in our syntax, but in our nomenclature..." :-)
Moose and Moo fail in practice to create maintainable code because of the meta stuff. Maintaining this "in production" is an absolutely nightmare (e.g., this "begin" thing - next we're gonna see an "in" and "around". Stick to clearly and directly constructing classes and resist making it an "twisted" application runner that's not practical in a maintenance situation - so I think this needs to be considered if there is going to be a successful framework emerge.
(for those following along at home, M. Conway and I are both well aware that the other has at least a modicum of experience with variegated codebases/teams/contexts etc. and so any mention of 'experience in practice' should be taken as contextual to the subset of the even wider variety that exists within the ecosystem that we happen to have encountered over past decades)
> And I agree that, if methods weren't BEGIN-lifted, my would be (technically) sufficient for class data.
My experience in practice is that sufficient code is written in that style -already- (package-level variables, has declarations, etc.) that we've fairly thoroughly proven that wrapping BEGIN {} around a (usually temporary) inline declaration is sufficient.
The need to be careful when using things with use or BEGIN is absolutely a bit of a wart on occasion but it's one users are going to run into anyway, and once you're using phasers, well, needing to use phasers is not honestly a major issue to people IME (and if we were to try to improve that, I think a core RFC that stands alone would be the right place to do so rather than Corinna - PEVANS/LeoNerd already has one in to provide BEGIN for compile time initialisation of 'my' variables, for example).
> And reusing my for class data also doesn't solve the problem of having to manually code every damn accessor for every damn class data slot.
Overall, I've found that when they're not purely for private storage, such data is best farmed out to a shared instance variable (often itself an object) that might be defaulted but is a first class concept in its own right.
This simplifies testing and also multi-tenant type stuff but honestly also it just gets really handy to, rather than having a single global X, having a default global X that's then trivial to override in general.
An example of this would be people using two copies of the same DBIx::Class::Schema object to run migration style ETL work, in situations where Class::DBI's use of class data would've rendered it painful at best - it's probably true to say that I implemented that feature more out of aesthetic preference than anything else, but it turned out to be extremely useful to early users and has only continued to prove its practical advantages since.
So I'm not convinced 'writing accessors for class data' is itself, directly, even a feature - writing a class data container where a single object contains such and using handles would tend to be what I'd default to just because it's saved me more pain than it's cost me lines of cost so many times at this point.
> We already have the slot keyword
We do, and I love it, and it is the one thing that is necessarily weird, in that 'slot $x = 3;' has the '= 3' part run at a *very* different moment to the RHS of any other such expression (but that part's great overall) ... but concomitantly adding onto this a behaviour of "oh, actually, if you add the :common attribute, this new and special piece of syntax operates in a *second* new ans special way" does not, honestly, seem an improvement.
The previous discussions on IRC and github were never about "there should never be a specific syntax for class data", they were always about "this specific syntax for class data is going to make 'slot' even weirder than it necessarily is to provide the good parts and since declarations of 'my' and 'state' variables within class blocks seem like they should do what the author means anyway, how about we don't try and complicate things further until we have experience in practice of where/how class data would even be used in Cor" (especially bearing in mind that the rest of the Moose and Moo maintenance teams consider class attributes to largely be a footgun, not just me).
Fundamentally, from where I'm sitting, overloading *slot* lacks intentionality far worse, and at least the use of 'my' and 'state' in the way I describe in the github comments fit how perl already works rather than piling special case atop special case in a design we're trying to keep to a "don't paint yourself into the corner" version 1.
Please consider this stanza of my response to be the one I consider truly important, since it's IMO the least subjective.
> Because software like PPI or PPR, which would be the basis of those tools, simply won't be able to tell whether a particular my is a class data slot, or just an ordinary lexical variable (perhaps, for example, acting as private shared memory for two closures within the class).
To me, that would be just as much class data as anything else, but if there's a principled distinction between 'class scoped variable closed over by methods' and 'class scoped variable considered as class data' that you have in mind that I've missed I'd be delighted if you could find the time to elaborate on what it is because having read the above sentence of yours multiple times I'm damned if I can see an effective way to draw such a line.
1) CLOS uses 'defmethodp foo :before ...' and I've yet to see any of my angry opinionated lisper friends be sufficiently upset by this to macro up something else
2) This child of two Eng. Lit. graduates dearly loves your proposed alternatives for *him* but fears how much fun they might not be for people for whom english isn't a first language
I express no specific opinion thereby, but I do think both datapoints will be worth bearing in mind in the process of developing an opinion to express.
> But can we really specify unlifted methods?
(And is "technically sufficient" really good enough?)
> Are people really going to be happy that they can't use objects in BEGIN blocks (or pass them as arguments to use statements)? Or can only use them in those contexts if the object's classes are themselves specified in BEGIN blocks or use-d from a module?
Hiding at the very end of this set of questions is the only part that matters in 99% of cases: almost all code using this will be using an object class defined in a different module. The rest are mocks for tests and people who already know better writing CPAN modules. Does not seem like a good optimization target for a core object system.
Moo doesn't actually have any 'meta stuff', that was rather the entire reason I wrote it and why calling ->meta on a Moo class creates an object that auto-inflates itself to a Moose::Meta::Class on demand.
Yes, what extensibility Moo *does* have has been used in ways I've not been entirely happy about by some authors, but I can't see a world in which "make it impossible to extend at all" would be an improvement.
However if you have specific worries in mind I'd be very much interested to hear them.
Thanks all, Matt; I only have experience with one codebase and I am pretty sure it was not done with a best practice in mind. Are there ways to create maintainable code? Yes. I have not experienced that, personally where I am - which tells me the the right thing to do is not the laziest and easiest thing to do. YMMV :-)
Also, I mistyped in my original comment that I think might have lead to some effort on the RTs (sorry). I meant before not begin. Sorry about that, good info nonetheless.
And yet I got enormous pushback against the idea of a separate declarator keyword for class data. Because, if slot :common is weird, and my and state are non-intentional,
a separate keyword for class data what we actually need.
I heartily wish that mythical extra keyword could be classslot, because that expresses the concept perfectly, but that triple-s just won’t fly. Maybe commonslot? Or
jointslot?
Of course, if the slot keyword were data or objdata instead, then we could use classdata for declaring class data. But I suppose that would be too easy, eh?
And, even if we do define a separate keyword for class data (now, or in some later evolution of Corinna), we’re still left with the inconsistency of method foo :common, and with having to explain why class data are declared with a keyword, while class methods are declared with an attribute modifier. :-(
Unless, of course we also change to method/classmethod, or even
objmethod/classmethod. ;-)
But, once again, that’s the argument for just using package and sub for classes and methods: that they do what the author means anyway.
The problem is that they don’t say what the author intended. And neither do my or state (though, admittedly state gets a lot closer than my).
OO languages have been around now for 50 years, and class data for at least 45 of those years. I’ve personally been using class data in OO code in multiple languages for at least 35 years. My guess is that class data would be used in Corrina in all those same places that I’ve used it for the past three decades. Granted, those usages have not been particularly frequent, but class data has been invaluable on the particular occasions I did need it.
Okay, so we have very different experiences in that regard. I don’t use class data very often, but I find it to be an excellent solution when I do use it.
Then I think we must have very different conceptions of what intentionality implies. To me, intentionality is the idea that constructs that are fundamentally similar in some way should be specified with a fundamentally similar syntax, and that constructs that differ in some way should be differentiated by a fundamentally distinct syntax.
Object data and class data are both, of course. They are fundamentally similar in being data that is part of the abstract representation provided by a class. They are fundamentally different in being encapsulated differently (per object vs per class).
So, to be declared intentionally, they each need a declarator that is syntactically similar (e.g. slot) and they need a differentiator that is syntactically distinct (e.g. :common, or the absence thereof).
Happy to...
In other words, while it’s perfectly true that class data slots and incidental my variables do both work the same, they each mean something entirely different. They have different roles and purposes, they inhabit different levels of abstraction in a design, they have different degrees of significance and permanence within the code, they are of primary interest to different individuals (the designer vs the implementer), and they need to be easily detectable and distinguishable by the disparate tools that those different individuals might wish to create and employ.
All of which is why they need different keywords too.
Extending the points that Damian has raised, one thing which has been brought up repeatedly about Corinna is a MOP. Most people don't need it, but when they do, they need it.
Have proper class data declarators that are part of the class definition can be represented in a MOP. my variables cannot.
But rather than leave it there, let me share a part of last night's IRC discussion (emphasis mine).
Thought mst and I often do not see eye-to-eye, in this case, I completely agree with him when he argues that things should be introspectible via the MOP and that we should avoid painting ourselves into a corner in terms of extensibility. Pretending that my variables are class data fails on both counts. Representing class data as part of the class does not.
This post sort of sidesteps the question of whether attribute syntax is a thing people want more of. Yes, perl does have a pattern of key/name/attributes/body forming, but it only recently formed, and I, for one, find the attributes to the left of function parameters so ugly that I've decided to stop using attributes entirely. But then, I also avoid using method modifiers in anything other than debugging, so maybe I'm not the intended audience.
On the whole, I prefer syntax that is minimal and consistent to the problem domain it describes, rather than consistent to other neighboring syntax.
> And yet I got enormous pushback against the idea of a separate declarator keyword for class data. Because, if slot :common is weird, and my and state are non-intentional,
a separate keyword for class data what we actually need.
Why not just use 'common' in that case?
If we have 'common $foo;' for class data that fits with 'common method foo' for class methods, in the same way that 'my $foo' and 'my sub foo' exist for lexical declarations currently.
My point that overloading 'slot' seems to me to be *less* clear than using 'my' would indeed be obviated by having a separate keyword that was actually clear.
Similarly, I wondered on IRC if we should have 'modify foo :before' rather than 'method foo :before' and got pushback from ovid because that would mean "we add five new keywords instead of four" which I found equally unconvincing.
(ovid did later mention 'modify method foo :before' which if anything expresses the author's intentions even better, but unless I misunderstood he didn't seem to feel that obviated the 5 vs. 4 argument, sadly)
>> ...since declarations of 'my' and 'state' variables within class blocks seem like they should do what the author means anyway,...
> But, once again, that’s the argument for just using package and sub for classes and methods: that they do what the author means anyway.
I think I was insufficiently clear there, *that* wasn't the argument I was trying to make - the argument I was trying to make was that 'my' should work no matter what we decide about class data (which, to my mind, strongly suggests -not- begin lifting fragments of the class block and instead having things work linearly - a bunch of IRC based pushback has been significantly over working around the fact that -bits- being begin lifted cause confusion by forcibly begin lifting -more- bits seems like going in the wrong direction)
No matter how we resolve this though, given that our experience does at least intersect at the first part of
> those usages have not been particularly frequent, but class data has been invaluable on the particular occasions I did need it
I still think that waiting a bit *might* be better than shoving it into version 1, though that's perhaps as much 'fear of backcompat traps' as anything else and it could certainly be argued I'm being overly paranoid.
Though as another possibility, given it's an infrequently used feature, one might even suggest that "making it possible to add a 'common' keyword" would be an excellent test of an extension system. Both Moo and Moose have extensions that provide a class_has keyword, and even if I prefer to avoid using them, I'm very much in favour of it being possible for them to exist.
Either way though, 'slot :common' seems like the worst possible option to me because of it making the 'slot' keyword doubly special, and I think 'common $foo' honestly sounds pretty good.
tl;dr - I think the only point where we meaningfully disagree about 'common $foo' (or an alternative named keyword) is whether it's *definitely* a good idea to have such a thing in core in the first version.
I also don't really see that class data is anything other than a global. You can describe them in lots of flowery prose, but in the end everyone in Java just used class static fields as global variables in all the same good and evil ways that C developers used globals. Perl already has 'our' for globals. If the problem is that people want to import the globals of a parent package for convenience, that can be done with the 'use' line. Or, maybe
our $var :inherit;
> Pretending that my variables are class data
To be clear, that has never been the argument.
The argument has *always* been that 'my' variables could be used for many of the purposes of class data and that that means that the decision as to whether to add a specific class data feature in the first version would have less of a cost.
>> trying to avoid finding we've painted ourselves into a corner
Ovid bolded this comment of mine for a related reason, but the whole point is that the -relatively- low cost of not formalising class data -yet- may mean it's worth not doing so immediately until we see usage patterns in the wild, precisely to -avoid- the inevitable risk of corner pointing that's inherent to codifying any particular feature for the first version.
> Happy to...
Sorry, I completely forgot to comment on your explanation of perceived differences, largely because your position on that now makes complete sense so I didn't really have much to add.
It does strike me that if we're talking about 'common $var' (even if under another name) being visible through the MOP for testing and debugging and general escape hatch purposes, then yes, that makes the case for it being its own thing much stronger.
I was under the possibly mistaken impression that there wasn't consensus for making private-ish things visible through the MOP at all so hadn't factored that possibility in.
Honestly (from IRC) -
mst: the more I think about it, the more 'modify method foo :before' and 'common $foo' do go with 'common method foo' actually seem pretty ok to me
mst: just let's un-begin-lift everything for consistency while we're there so only 'slot' needs a weird initialisation order
LeoNerd: I'm still finding I like the braces in Object::Pad's has $slot { init-expr here };
mst: yeah, so do I, but I also feel like people are going to -want- to type '=' and it may be worth letting them ergonomics-wise
mst: maybe that's the wrong choice
mst: going for a block so it looks like the separate thing that it actually is definitely has advantages
So something like:
class Foo :isa(Something) {
my $internal = 0;
common $state; # MOP-visible but otherwise private
slot $data :accessor { 0 }; # instance variable
common method foo (...) { ... } # class method
method bar (...) { ... } # instance method
# modifier applied to superclass method
modify method baz :before (...) { ... }
}
seems like it maximises intentionality.
(I continue to like Ovid's choice of 'common' to indicate class data/methods are accessible to instances as well as straight-up class calls)
Assuming we accept that there's no reason to beginlift 'method' in this style then - other than the fact that role composition will have to happen at the end of the block (which was always stevan's dream anyway) - the ordering of everything is extremely clear.
I especially like that this way '=' always means what you expect it to mean, happens exactly when the variable's initialisation statement is hit, and then the block for 'slot' gives a nice hint it *specifically* will get initialized differently.
Curious what people think.
Note I'm aware this is now arguably slightly less KIM but given Ovid's suggestion that 'modify method' was an improvement over 'modify 'I assumed this wasn't a strict requirement.
It also seems to me that the existance (and IMO elegance) of'my sub' is a good argument for 'common method' at the very least. "Scoping a thing" and "the thing" seem like they should be allowed to be considered separate syntax.
Rather than respond individually to Matt’s many valid points — and his graceful concessions — let me simply summarize my current opinion.
I concede that my sub is at this point a well established prior art for prefix declaration modifiers (even though I still think it was a misstep for Perl, syntactically).
Therefore I also concede that common $foo and common method foo would be an acceptable (albeit non-KIM) compromise, and would give us everything we need in Corrina v1, even if we can’t yet agree on the issue of BEGIN-hoisting.
BTW, I’d be even happier with that decision if the slot keyword had been named field instead. Because “field” is another frequently used nomenclature for an instance data variable, and even has history in Perl usage in the form of the use fields pragma. But, far more irresistably, because the agricultural analogy between a “field” (i.e. an individual’s personal allotment of property, reserved for their own use) and a “common” (i.e. a village’s communal allotment of property, whose use is shared by all) is...utterly perfect. ;-)
As for the modifier method foo :before vs method foo :before syntaxes: there I’m far less convinced. But I suspect that, in part, that’s because I am far less convinced that Corinna v1 needs this construct at all. I’d argue that this feature, rather than class data, is actually the one that is “...worth not doing immediately until we see usage patterns in the wild”.
But, if the wider consensus is that we do need method wrappers in Corinna v1, then the keywords for that construct definitely need better names. I’d suggest either the KIM syntax:
...or, if we are indeed giving in to the insidious creep of the <modifier> <keyword> syntax, then:Note that I still think these particular features could be deferred but, if they have to go in now, then they ought at least go in eloquently.
I’d suggest that in
before method foo …
etc style code,method
is no longer a keyword, but part of the identifier or ‘place’, analogous tosetf
in lisps.If you’re going to redo before/after/around, then I have a sneaking regard for lisp style named advice, so
could become:
Also, if we’re borrowing from Lisp, then before/after/around could (should?) be joined by
before-while
(if the advice returns false, don’t call the advised function),before-until
(only call the advised function if the advice returns false),override
(simply replace the advised function),filter-args
andfilter-return
(massage the arguments or return value).Yes, all of these extra modifier types can be implemented in terms of around, but I’d suggest that being able to more finely define up front what a piece of advice is going to do will help make things slightly easier to reason about.
> I concede that my sub is at this point a well established prior art for prefix declaration modifiers (even though I still think it was a misstep for Perl, syntactically).
Maybe it was, but we want to achieve something that's both good -and- going to feel natural to existing developers. This does not at all mean I'm not overall in the wrong by using it as a precedent, mind.
(raku's making the sub keyword fully lexical is still something I look upon with envy, and I hope if I call you spoiled in that regard it will be taken in that spirit ;)
> even if we can’t yet agree on the issue of BEGIN-hoisting.
It occurs to me that I've failed to explicitly say something I consider important:
I don't actually entirely object to the idea of beginlifting, what bothers me is doing so for *parts* of the 'class' block so you have to remember which keywords are beginlifted and which aren't.
While I am not fully convinced that it's a good idea, beginlifting the *entire* 'class Foo { ... }' construct would avoid all of my inconsistency complaints and I suspect also achieve most (all? answers on a postcard) of the desired goals. I could happily live with named classes being beginlifted - I just don't think that 'some parts of a class block are beginlifted, please memorise which' is particularly a service to our users. Either do it for the whole thing or don't do it at all, please, is basically where I'm at here.
> BTW, I’d be even happier with that decision if the slot keyword had been named field instead
I have never at all liked 'slot' as a keyword and while I'm quite comfortable with our theft-from-raku of 'has' in most current code, I actually really rather like 'field' as an alternative since it feels much more in keeping with 'common' linguistically even before we get to the (admittedly glorious) analogy. To the point where, much though I love 'has', I'm not sure if 'field' plus 'common' isn't actually strictly better than 'has'.
> But, if the wider consensus is that we do need method wrappers in Corinna v1
I fear the discussion around this has become unfortunately confused.
The initial argument was people saying "method modifiers are useful and important to us" versus Ovid saying "a method composed in from a role should not be allowed to access the existing method of a superclass at all" ... and these two extreme positions set the terms for the debate. Initially people argued for having some way to achieve that goal, were told that there wouldn't be any way at all to achieve that goal because it was bad and wrong to ever want to, and the result was a design that provided no such way followed by sufficient pushback that the maximalist approach to achieving said goal got added to the design instead, perhaps unwisely.
I believe that the majority of people involved simply want *some* way for a method composed in from a role to decorate/advise the method of the same name from the superclass, and that while raku grade redispatching might be rather nice, simply permitting '$self->next::method' to work in role provided methods would likely be quite sufficient.
In fact, it strikes me that assuming that *is* permitted, both before and around methods become trivial and the one annoying case that is the after modifier - i.e. running code after the call to the 'real' method while maintaining context for its return value - is already solved by LeoNerd's defer implementation and so can be handled by the perl RFC process without needing to directly impact the design of Corinna itself.
So, personally, provided roles aren't explicitly forbidden from providing modifier-like functionality, I would be entire comfortable with descoping modifier-like *sugar* from v1.
> versus Ovid saying "a method composed in from a role should not be allowed to access the existing method of a superclass at all"
That was unclear. 'existing/inherited method of the same name' would have been more clear.
Ovid and I have both suffered from the whole "having more passion than social skills" failure mode over the course of this process and clarity about the technical points is important; as such, I apologise to anybody who read that sentence the way I didn't mean and didn't realise was readable thereinto until after I'd already hit Submit.
I still don't get why we are redesigning everything from scratch rather than simple starting with the obvious thing, which is to take Moose (the far and away clear winner on CPAN in terms of popularity and maturity) and just clean it up a bit, perhaps modernize the syntax a little. What we seem to be designing here is something that is vastly different, completely incompatible and yet somehow lacks most of the features I get with Moose already. The main issue with Moose is that since it's not baked into the language it suffers from performance issues and dependency size issues (and due to both of those you get memory bloat). If I was doing this project I'd start with Moose and focus on how to solve those problems, ideally integrating into core would have a solid impact on them.
Why you all think ignoring 10+years of prior art and existing examples on CPAN and in the wild is a brilliant idea?
I will be surprised if any answer is given to your questions. Obviously, they chose not to go the Moose-way but there is a rationale section if you haven't read it:
https://github.com/Ovid/Cor/blob/master/rfc/overview.md#24-rationale