-
Notifications
You must be signed in to change notification settings - Fork 19
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Clarify the scoping nature of fields #61
Comments
Given the questions above, there are now a few ideas for test cases. In each case, what should the correct behaviour be? Should it be a compiletime failure? If not and it does at least compile, what would the correct result be? Duplicated fields in the same block
Duplicated fields in their own block
Class code split across two locations
Given the competing concerns, they can't all pass - the model implied by test 2 passing means that test 3 must fail and vice versa. So what do we prefer? |
My personal feeling is that we should use model 1 ( |
I much prefer the class Example {
method calculate_thing {
field $cache;
return $cache //= do { expensive code goes here };
}
# other code and methods here can't see $cache
} But use an outer block if more than one method needs it: class Example {
{
field $cache;
method calculate_thing {
return $cache //= do { expensive code goes here };
}
method needs_the_cache {
my $foo = $cache->stuff;
}
}
# other code and methods here can't see $cache
} |
For duplicated fields in the same block: class Test1 {
field $x;
field $x;
} At minimum, that should be a "redefined" warning, but I think it might be better if it's fatal. Much safer that way because it's easy to overlook/swallow warnings. For duplicated fields in their own block: class Test2 {
{ field $y; method one { $y++; return $y; } }
{ field $y; method two { $y++; return $y; } }
} With your proposal for the class Test2 {
method one { field $y; $y++; return $y; }
method two { field $y; $y++; return $y; }
} I think that gives us a powerful new mechanism for managing state, and even allowing it to not be exposed to other parts of the class which doesn't need it. Encapsulated encapsulation. That being said, what does this mean? method one { field $y :param; }
method two { field $y :param; } To my mind, any For class code split across two locations: definitely a compile-time error. |
Also, this is the full list of I realize that not all will be implemented. |
I wrote this: For duplicated fields in the same block: class Test1 {
field $x;
field $x;
} At minimum, that should be a "redefined" warning, but I think it might be better if it's fatal. Much safer that way because it's easy to overlook/swallow warnings. Now that I think about it, it must be an error because a warning would be a disaster. For regular Perl, we have this:
And that generates a warning, but it's usually harmless. In fact, you can usually just remove the The reason this is harmless is because in procedural code, this is just a control flow issue. You've gone straight from the first to the second class Example {
use Some::Other::Class;
field $answer :param :reader {42};
method frobnicate ($val) {
return $val < $answer ? "frob" : "nicate";
}
# much later in the same class
field $x { Some::Other::Class->new }
method uh_oh ($thing) {
if ( $x->handles($thing) ) {
...
}
}
} What does that second If we make this fatal instead of a warning and need to back it out later, no one will be relying on that behavior. However, if we make it a warning and not fatal, someone might rely on that behavior and if it turns out to be a serious design flaw, we might be stuck with it. I recommend playing it safe and making that fatal. |
Yeah that makes sense. Once you can hide a
Certainly I think those ones shouldn't be permitted - |
Allowing several fields in a class to have the same name like this feels ... itchy. With Object::Pad an object can have several fields with the same name - iff these fields come from different parent classes or roles. This is an enhancement compared to Moo*, and it makes some sense. It is still possible to describe an object's state in words if you think of the field name as its "given name" and the class/role holding its definition as its "surname". What Perl's traditional OO (including Moo*, but not inside-out) has given us for free is an incredibly easy and robust way to serialize objects, and I am probably not the only one who uses that a lot. Part of it is already gone with Object::Pad: Data::Dumper can not round-trip Object::Pad objects. Even if it could be taught to serialize (that can be viewed as "describing an object's state"), de-serialization is no longer that simple. You can, of course, go Meta with Object::Pad for serialization, and I have some hope this will be available for Corinna. But can you? How do you call Anecdotal evidence: I had to go Meta for serialization to be able to convert between different versions of an Object::Pad class. Storable sort of works sometimes, but fails in interesting ways if the order of field declarations in the class is changed. |
I think the general rule would be: if a |
That's a thorny question. At first blush, I would suggest that it only pulls top-level fields, but if you want to use the MOP to clone something, you're stuck unless we find a clean way to declare scope in the MOP and that's not good. For now, I would suggest that duplicate names are forbidden until we hash this out. It's the simplest way to resolve the issue (though I can still see the scope issues). class Blue { field $f; }
class Blue { method m { $f++ } } But let's look at procedural code: package Blue {
my $f = 3;
sub foo { say $f }
}
package Blue {
sub bar { say $f }
} That won't work and I'd suggest that, by analogy, it wouldn't work in Corinna, either. Lexical scope is lexical scope. But yeah, this is a problem: class Example {
field $x {2};
{
field $y {2};
method foo {
# can access $x and $y
}
method bar {
# can access $x and $y
}
}
method baz {
# can access $x, but not $y
}
} Thus, in the MOP, we'd need some way of saying which methods can access those fields. Either "all" (the common case) or a list of methods, identified by signature (because eventually we're likely to get multisubs and (hope, hope, hope), return values can be included in the signatures). |
I agree (even if you drop "For now"). The benefits of using duplicate names are too small.
And then we might want lexical methods so that method names are no longer unique. I think the chances that this gets into the way is bigger than the chances that it actually helps to implement interesting things. I also would forbid more than one class block with the same name. A package is just a namespace, so there's nothing Perl can do about it, but IMHO a class should be "sealed" when the declaring block is closed (unless Meta wizardry is applied). In that case there are no scoping issues with fields. |
For now. In the future, it's entirely possible (even preferable) we can disentangle classes and packages. @leonerd Thoughts? |
I think for now it's fine to make it an error to define a field with a given name multiple times per class. But to make sure everything else in the implementation would allow it, so we don't paint ourselves in a corner. Things like anonymous classes or roles may be desirable in the future, which could easily make it impossible to have a unique 'name' for a given field. |
Might I toss in a 3rd possible way to look at field declarations? 3.
|
Another thing to consider with regard to scoping: Currently Corinna has no way to make a field visible to sub-classes unless this field is at the same time made available to the world at large with a method (same for Object::Pad). This is no decent encapsulation, and in my opinion worse than having a field always visible to the whole code of the current class. If this is ever to change, we need something that can be made visible to the sub-class, and therefore outside of the class where it has been declared. This makes it different from |
@leonerd started this with:
This reminds me to a discussion in #44 ("
This also applies to a |
Hrm; @HaraldJoerg makes a good point. If they do scope the same way as |
I will say that nonlexical fields are likely incompatible with being able to split class definitions, or really any other situation in which variables can be "pushed" into scope from a distance (such as subclass-visible fields). Consider a variation of @leonerd's example of doing this:
What should By requiring the 2nd block to restate My gut says subclass-visible fields are best done with |
Thinking further about inheritable fields for subclassing, I think it can be done within the syntax, while still keeping a lexical-like model. The trick is to be explicit about which fields are being inherited, rather than just pulling them all in by default. That feels more in keeping with the "nothing-by-default" way these all work. I've written some notes about it in the Object::Pad queue, where I'll have a go at it first. https://rt.cpan.org/Ticket/Display.html?id=143645 Anyway, because of that, I don't think we really need to consider too much how inheriting these will work at the moment, because the explicit keyword approach probably solves it better anyway. |
Various thoughts above seem to be converging on the idea that we can have both toplevel fields at full class scope, and buried fields within smaller scopes or individual methods. I'm a little concerned we haven't been very clear on how this will work. For example, it's fairly clear that
I think most folks would want the C2 example to be toplevel. Most folks would consider the C4 example to be buried. So what about C3? That single semicolon might make a lot of difference here. I wonder if it's a bit subtle and hard to explain. |
I'm still not convinced we need them, but they're effectively protected attributes, similar to what Java offers via the
But ... you can find plenty of people arguing that protected fields are better than private. Plenty of flame wars on that front. When Java first came out, many developers simply declared their fields as public, because hey, we have types and that saves us, right? You can imagine how well that worked out. So they learned to make their fields private and only provide readers/writers if needed (and eventually built tooling to automate that). Making my fields directly available to a subclass feels like the same thing: a massive encapsulation violation and now I've tightly coupled the classes. Or the subclasses can use the properly defined interface and never have to worry because you've silently changed this: field $name :reader; To this: field $item_name :reader('name'); |
Update: Somehow I thought this was on the implementation discussion board, not a discussion issue. This makes part of what I've written in the following irrelevant. i've been thinking about this a lot and I've reached some tentative conclusions. I really like a lot of the ideas being discussed here. That being said, @leonerd asked about these cases:
Now I'm confused. Corinna deliberately uses a postfix-block syntax for the initial pass to ensure we have no leakage of the The above appears to suggest we're now considering something file-scoped. As near as I can tell, this violates what the PSC asked for. When I spoke to them about including Corinna in the core, I was very disappointed that they asked for a much more limited breadth of work (I say "breadth" rather than "scope" to avoid confusion with "file-scoped" or "block-scoped"). However, I have to admit that the PSC was correct. The smaller the scope, the fewer bugs we're likely to have and the easier we can test the overall behavior. We've had enough bugs even with smaller, simpler features (pseudohashes, try/catch, etc) that with a change as large as Corinna, it's worth being more careful. @leonerd also wrote:
I'm very concerned about this. We spent years trying to get a good, solid definition of Corinna and much of it follows the developer's motto: "we do these things not because they are easy, but because we thought they are easy." There's still plenty of edge cases we haven't considered (introspection hasn't been fully-defined, for example). I think some of these ideas considered here are powerful, but if we get the specification wrong, we not only risk introducing serious bugs, but could also risk the second-system effect. I think our principle of parsimony is our friend here. In short, don't put in changes we can't back out of unless those changes are absolutely necessary for Corinna to function. If we later find out that we're being too restrictive, we can loosen up those changes. But if we loosen them up at first, we don't want people relying on what could be a mistake (or worse, discovering we could have done it better in a different way, but one that's not compatible with previous choices). So to reiterate what I think would follow from this:
Yes, it's frustrating, but it's safe and we can revisit those ideas later when we see how the system works as a whole. |
That's why I like @leonerd's suggestion to have both parent and child class explicitly declare that. In my opinion, inheritance should only be used if there is some coupling between the classes because there's always the chance that a change in the base class steps on the sub-classes' toes. You should be able to say Example: A If I can write something like I do not request unconditional availability of fields to sub-classes. But right now the language forces me to widen the public interface, and that's something I'd like to see changed in the future. |
@HaraldJoerg If we go this route, I would instead suggest something like this: class Box :isa(Object) {
# field attributes only permitted on scalars
has $edges :writer(:trusted) :reader(:trusted);
}
class Polyhedron {
method surface_area () {
my $edges = $self->edges;
...
}
} In the above, by declaring |
Per convesation with @leonerd on IRC:
That makes perfect sense and I misunderstood that. |
That would be quite a significant departure from what we have now. Currently Perl classes (of any flavour - classical or Corinna-shaped) don't have the concept of methods that aren't a public free-for-all. We could consider what it would mean to have non-public methods, and then the idea of non-public accessors for fields might fall out of that; but that feels like a different path than also exploring the idea of optionally opening up field visibility to subclasses. |
Other thoughts (copied from IRC) on the subject of inheritable fields: If inheritable fields exist, they provide a way for subclasses to provide different initialisation expressions for fields:
Without inheritable fields, it's hard to imagine how to write that without making some sort of builder method. About the best I can come up with is
which feels lot worse. It's syntactically noisier (pardon the phrasing), it exposes extra methods in the API (even if they're discouraged from public use by that leading underscore), and it's much slower to execute because now the constructor has to perform a dynamic method dispatch, whereas previously it did not. |
I don't understand that. Corinna defines So the pros of making variables inheritable:
And the cons?
One of the primary ideas of Corinna is to offer affordances for safe behaviors, but letting the developer go through just a touch of extra work if they want to do something bad (e.g., the builders in @leonerd 's last example). Fundamentally, this is the reason why Alan Kay left inheritance out of the definition of OO. The idea of "subclasses as specialized parents" isn't enforceable. My We know how troublesome inheritance is, to the point where some OO languages (Go, VB variants) don't even allow it. I don't advocate eliminating inheritance, but we shouldn't be encouraging it by making bad design decisions more convenient. Unless ... What would make me change my mind and be more open to inheritance would be anything, ANYTHING, ANYTHING which would make it safer. We all know that we shouldn't Between safe and easy, give me safe. For small systems, this isn't as much of an issue, but systems grow and it becomes an issue. I work on big systems and issues like this are a train wreck. So what can we do to make it safer? Types/type constraints would be a huge boon, but we don't have them. Some way of enforcing contracts? Nope. Most developers still don't understand the Liskov Substitution Principle, so dropping an instance of a subclass somewhere where we have an instance of the parent isn't safe. So we have no safety at all in inheritance, but least I can do this: class ConstrainedPoint {
field ($x, $y) :param :predicate :reader; # no defaults, so it's not *really* safe
field $limit :param {10}; # oops, no types :(
method set_x ($new_x) { if ( -$limit < $new_x < $limit ) { $x = $new_x } else { die ... } }
method set_y ($new_y) { if ( -$limit < $new_y < $limit ) { $y = $new_y } else { die ... } }
}
class PointWithDefault :isa(ConstrainedPoint) {
ADJUST () { # use default
$self->set_x(0) unless $self->has_x;
$self->set_y(0) unless $self->has_y;
}
} In the above, I could have just used the class PointWithDefault :isa(ConstrainedPoint) {
field ($x,$y) :inherit {0}
} But now, any code in If you can provide a way that |
I think another way of putting this: I would much prefer if inheritance in Perl could be more akin to a subtype. In a subtype, you know that it respects the parent's contract. In Perl, a subclass is just syntactic sugar for reusing some of the parent's behavior. But many developers don't realize that. In fact, we often assume that a subclass would be a subtype, but there's no guarantee of this in Perl, so our expectations don't match our reality. But I also see no way we can get our expectations to match our reality because currently, Perl is so crippled by lack of types that building large system is hard. However, Moo/se along with Type::Tiny has made this so much easier and safer, so to a certain extent, Corinna is a step backwards (albeit a temporary one). Since we can't have types or contract enforcement, don't give the person writing a class tools to make the issue worse. |
For whatever it's worth, my own view on these topics is very clear. 30+ years of writing and teaching OO has convinced me beyond a shadow of a doubt that providing any kind of mechanism to access encapsulated data outside its own class...is a disaster. Even when such a mechanism is provided with only the very best intentions and in the expectation that it would only be used in cases of dire need and design extremity. Whenever I teach OO design or implementation, I make it crystal clear that allowing any other class or external code to directly manipulate the internal state or implementation of an object (whether that's via protected inheritance [C++], explicit trust mechanisms [Raku], or just inadequate encapsulation semantics [Perl]) is a critical design flaw: either in the OO language itself, or at least in the (mis-)use of that language within a particular OO design. Yes, it can be extremely convenient and efficient to access a base class's fields from within its derived classes. But in my experience of my own systems, and the systems of hundreds of students and clients, eventually such direct accessing leads to subtle bugs and irretrievable painted-into-a-nasty-corner situations. I am so adamant about this, that I even argued against In designing Corinna, we did everything we could to discourage inheritance, having learned from long and bitter personal experience that inheritance cannot successfully be made to serve the two incompatible masters of interface consistency and implementation sharing. Inheritance of implementation is a failed model, which is why we see the ever-increasing popularity of aggregation (role-based) mechanisms instead. Hence, I strongly believe that we should not even consider the possibility of providing at some future date a mechanism to make fields accessible outside their classes. (Having ranted sufficiently on my personal OO bête noire ;-) let me address the original question(s) posed in this discussion. I strongly believe that:
Most of those beliefs follow self-evidently from my preceding rant, but I do need to clarify the very first point (and my response to the original question): why I think fields should be strictly lexical, rather than method-like or package scoped... Good OO design is all about maximizing encapsulation and decoupling, by minimizing the number of code lines/scopes/files in which internal object state is accessible. Making fields lexical accomplishes that goal: the particular piece of state stored in a field variable is only accessible from the line it was declared to the end of the surrounding block. That's simple and predictable for users and can be efficiently implemented as well. It allows different items of object state (i.e. fields) to be segregated into separate sub-blocks, thereby minimizing intra-class coupling and reducing the chance of unintended enbugging within a single class. You simply can't accidentally mess up the state of a field if that field isn't even in the lexical scope of the current method. If fields were to have method-like scope, then that greatly complicates the compiler's task of checking and resolving field names across separate scopes. And, more importantly, it greatly complicates the user's/tester's task of making the same checks and resolutions. Moreover, it immediately eliminates any possibility of restricting a given field to anything less than an entire class scope, which removes a powerul and safe tool for decoupling and encapsulating state within a class. For these same reasons, it would be disadvantageous to implement fields with package-like scope. Moreover, if fields are anything other than lexically scoped, and if we're going to allow split class declarations (but please don't!) then we're explicitly providing a mechanism to allow users to completely side-step encapsulation.
If Whether it's this evil trick, or an official syntax for An OO language can only be as good as its worst feature, and public/protected data is by far the worst feature of most existing OO languages...including "classic" OO Perl. So please let's not add "modern" OO Perl to the ranks of the damned. ;-) |
Damian (@thoughtstream), thank you for that. Side note: Corinna switched from multiple inheritance to single inheritance and now I'm beginning to regret any inheritance in its design. It's too buggy, doesn't do what people think it does (particularly in a language like Perl), and people abuse it for all sorts of uses. Sadly, I've had some larg(ish) OO systems I've tried to evict inheritance from and I've found edge cases where it's really not feasible. method foo($bar) {
# some code here
$self->next::method($bar);
# more code here
} Inheritance gives me very fine-grained control over the timing of when I can call a superclass method. Roles make it much harder. That being said, I'm still sorely wishing I could have killed inheritance entirely, but I was pretty sure that Corinna would have been rejected had I gone that far. |
I am a bit tired of hearing this argument. In Perl, The bignum "types" and things like Type::Tiny make the border between types and objects a somewhat blurry one. They also nicely show the possibilities - and limits - of this approach. As shown by your
That makes it another overlap between types and objects. In my understanding the wording
I also agree with the observation:
Indeed, Perl is not particularly good at enforcing things. Corinna can encourage the use of inheritance as subtypes by wording (
I am including those classic quotes because I think that Corinna is bound by the spell "Perl will stay Perl". So what if we drop inheritance? Inheritance isn't for formal strictness, it is for convenience. I am aware that this convenience has been used and abused in various ways. I can write my example without inheritance: class Polyhedron {
has $object { Object->new }
has $edges :param { [] } # was: has $edges :inheritable
}
class Box { # was: class Box :isa(Polyhedron)
has $polyhedron { Polyhedron->new( edges => [...] ) } # was: has $edges :inherit { ... }
} That way, the I understand that it is very desirable to read code which has been written using a clean OO system. But I also expect that the motivation to write code for a too restricted system in the first place isn't overwhelming. Object inheritance has been available to Perl programmers for quite some time now. Not including it in Corinna because it can be abused or because Alan Kay didn't like it seems to target the wrong audience. |
As regards @leonerd's Cat/Lion example... I understand why My first issue is that the hierarchy itself is wrong. A lion isn't a cat. Not in the housecat-that-meows sense. Which means our inheritance hierarchy should be more like this: class Feline {
method greet { say $self->sound }
method sound; # abstract
}
class HouseCat :isa(Feline) {
method sound { 'meow' }
}
class Lion :isa(Feline) {
method sound { 'roar' }
} But this shows us that both So greeting is really a role they perform: role Greeting {
method greet { say $self->sound }
method sound; # abstract
}
class HouseCat :does(Greeting) {
method sound { 'meow' }
}
class Lion :does(Greeting) {
method sound { 'roar' }
}
class Raven :does(Greeting) {
method sound { 'Nevermore.' }
} As for the original space-vs-time efficiency tradeoff...that's only necessary because Corinna currently lacks parameterizable roles. In Raku, for example, the same classes could be implemented without the performance hit of calling the role Greeting[$sound] {
method greet { say $sound }
}
class HouseCat does Greeting['meow'] {}
class Lion does Greeting['roar'] {}
class Raven does Greeting['Nevermore.'] {} If we're thinking about future compatibility, |
I recently started writing something sufficiently complex with the Let's take Damian's most recent example1: role Greeting {
method greet { say $self->sound }
method sound; # abstract
}
class HouseCat :does(Greeting) {
method sound { 'meow' }
}
class Lion :does(Greeting) {
method sound { 'roar' }
}
class Raven :does(Greeting) {
method sound { 'Nevermore.' }
} Every class must have a I agree with Damian that the correct answer is Polymorphic Traits, but then I've read SCIP twice (most recently the Javascript version) and I'm all aboard the substitution model party bus. But, I'm not sure when we will have polymorphic traits simply because we don't generally have polymorphic modules in Perl so it's not a common concept for Perl programmers. IMO we should have a solution to this that isn't polymorphic traits, and doesn't require polluting the public API for state maintenance that should be internal to the class. I think we do, and I'll get to that in a minute but first let's start with Damian's … vhement … commentary on encapsulation.
I entirely agree with him, and it's because I agree with him that I really dislike the solution of having methods simply to transmit state for inheritance. But I disagree that this is violating the encapsulation of the parent class because Perl isn't Go. In Go you define an object by declaring a struct and methods that act upon that struct: type base struct {
value string
}
func (b *base) say() {
fmt.Println(b.value)
} You can do "inheritance" in Go by embedding a struct, the child struct can be substituted for the parent and it will simply pass all calls along to just that embedded struct. type child struct {
base //embedding
style string
}
func main() {
base := base{value: "somevalue"}
child := &child{
base: base,
style: "somestyle",
}
child.say()
} In this system the border between what is the child and what is the parent is very obvious, and the encapsulation between them makes some sense. They're fundamentally different data structures. Perl also isn't Java. Java is a bit more muddled but still has a distinct line between parent and child classes. class Vehicle {
private String brand = "Ford";
public void honk() {
System.out.println(this.brand + " says Honk!");
}
}
class Car extends Vehicle {
private String model = "Mustang";
public void honk() {
System.out.println(this.brand + " " + this.model + " says Honk!");
}
public static void main(String[] args) {
Car myFastCar = new Car();
myFastCar.honk(); // throws error: brand has private access in Vehicle
}
} The Perl doesn't work that way, nor is it just an implementation detail … we chose to have it work this way. Primarily because until now we chose not to have attributes as first class citizens, and now because we're choosing not to have constructors.
I think the time for enforcing a distinction between parent and child class data was in 1993 when we chose not to have first class object data to begin with. I like Aquanight's suggestion about
B doesn't just point to A … it pulls a copy of the Returning to the problem at hand, I think the Cat example is doing us a disservice because it leads us to think that our imperfect metaphor is the problem (e.g. Not All Cats). I'm going to replace it with an example from some recent code of mine.
The
Now I can choose my public API and which pieces of state I wish to encapsulate independently of each other. I remember being distinctly frustrated when I wrote the ECS::System code because the first idioms I reached for didn't work and then I had to implement what was in my mind a kludge. I remembered having these discussions and saying "let them eat cake" when it wasn't my head on the block, I think however the lexical alias to instance storage makes the most sense to me … and if we want to make a more solid distinction between parent / child /role data … we need to discuss that at a more fundamental level because we've got a much bigger change to Perl that needs to be made. Footnotes
|
I think we have an X/Y problem, but it's not your fault. There's a fundamental issue in Perl that keeps cropping up, we've never fixed it, so we just ignored it.
Sorry for a very tiny nitpick, but I think it's important here. The The unavoidable coupling between the classes occurs in the constructor (perhaps this is a design mistake). The avoidable coupling occurs when developers choose to expose fields via
This is where this breaks down for me. Let's say we do that. and let's say I decide to port My hypothetical And then the hate mail comes in because people have been relying on my private implementation and they can't do that with the XS version. A class is only there to share its API. The class is not just a bundle of state with some behaviors on top. Inside of the class, the state and behavior are tightly coupled and allowing you to mess with one without respecting the other is like changing the spark plugs in a car and installing copper spark plugs without knowing the engine requires platinum spark plugs. Encapsulation is about respecting that coupling of state+behavior and only relying on the public API. That goes for subclasses, too. In fact, inheritance should be about sharing an API, not the implementation, but I won't win that fight (and I confess I often don't respect that for my own code, so it would be a weak argument, but then, I try to avoid inheritance now). The real problem is the problem that Perl keeps running into again and again: respecting boundaries. If I publish some code, some rando out there can do just about anything they want to with it, including doing things against my will. By the dying light the seven sublimating deities of the twilight realm, do not touch my privates!Call me Woke. But meh, do whatever you want with your own privates. That's your business, not mine. Your Perl can be TIMTOWTDI, but let me trust my own code. And that brings us to what I think is the X/Y problem: class JSXL::Extended :isa(JSXL) {
...
} If As an aside, if we think of web servers and web browsers as objects, they are completely separated by an API. They provide isolation, a complete hiding of state+process which goes beyond encapsulation. It's so powerful that if the web server crashes on a request, your browser does not. However, I never proposed that for Corinna because I am a Bear of Little Brain and I had no idea how to do that. Getting back to our problem. I've given plenty of thought about how this could be accomplished. I toyed with class JSXL {
# field declarations
# method declarations
class JSXL::Extended :inner {
...
}
} In the above, But as systems grow, that's not going to be scalable. You really should not have deep object hierarchies and should instead resort to composition over inheritance. But Perl is Perl and Perl is the Perl community, so this might be an acceptable compromise: class JSXL {
# field declarations
# method declarations
class JSXL::Extended :trusted;
}
# in another file
class JSXL::Extended :isa(JSXL) {
...
} In that example, the Thus, the owner of However, if someone goes into And they leave the company and someone else comes along and updates to a new version of This, in fact, brings us to a second problem that I've wanted to deal with, but we can't do that right now. Imagine a requires_verified JSXL => 'v1.2.3'; Imagine if that pointed to a canonical, secure set of SHA-3 hashes. When They could then hack the At the end of the day, we can't build perfectly secure, safe systems, but we should at least provide tools which make it easier for me, as a module author, to know that I can provide an interface and have the freedom to change internals. Later, we want a way for you, as a module consumer, to know you're getting only the module you requested. In other words, we can respect boundaries both ways. Your ExampleLooking at your example (and yes, please bring an ECS system to Perl! Pretty please!) class Games::ECS::System {
field $ecs;
method ecs($new=undef) { $new ? $ecs = $new : $ecs }
method components_required { [] }
method update(@entities) { ... }
}
class HealthBarRendererTest :isa(Games::ECS::System) {
method components_required { [qw(Position Health)] }
# ... removed test-harness code
} Here's what I would do (assuming I've understood correctly, which might not be true): class Games::ECS::System {
# this should probably be a method for strict validation
# because we do not yet have data constraints
field $ecs;
field $components_required :param = [];
method update(@entities) { ... }
}
class HealthBarRendererTest {
use Games::ECS::System;
field $ecs = Games::ECS::System->new( components_required => [qw(Position Health)] );
... more code here
} In the above, we compose rather than inherit, and we rely on the public interface without violating encapsulation. Further, if other people are allowed to instantiate |
As an aside, my "compose rather than inherit" example might be even more generalizable if we use dependency injection, but it really depends on how the class is set up. |
Ok, this is verging on necro-threading but I honestly have been trying to formulate my thoughts for a response. It boils down to two problems I have, one technical and one philosophical. Problem 1: Technical
One of the criticisms leveraged, correctly, at Moose was that it required the creation of Public APIs that had no purpose other than to simplify internal logic. has _bar => (
is => 'ro', # now my class has a public `_bar()` method for internal purposes
lazy => 1,
default => sub { HTTP::Thin->new()->request(GET 'http://example.com')->as_json() }
); Basically you're saying inheritance shouldn't/cannot violate encapsulation without explicitly creating behavior to mediate it. Unfortunately currently the only solution is to have public behavior to mediate private implementation details from parent to child. Yuck. I think you (@Ovid) agree with "yuck" because you mention how you've gone through a lot of exercises to claw back some semblance of encapsulation via I'm utterly onboard with the first part, state access must be mediated by behavior; I'm utterly appalled with the second, behavior must be public. We also have a a second problem … Problem 2: Philosophical
What is an instance? Given the following code (borrowed just now from an early draft of a tutorial): class Action {
field $entity :param;
method log_action($logger) {
$logger->printf('%s performed %s', $entity->name, blessed $self);
}
method perform() { ... }
}
class MovementAction :isa(Action) {
field $dx :param;
field $dy :param;
method perform() { # TODO: $entity->move($dx, $dy) }
}
my $action = MovementAction->new(entity => $player, dx => 0, dy => 1); What is I think ultimately the answer is that inheritance is a mistake period. Unfortunately we're not off the hook because we're going to see similar problems with private state in roles which in my experience have similar issues with behavior3. State should be lexically scoped, but also sometimes needs to be communicated via non-public channels in a controlled manner. Ultimately any mechanism we include to provide polymorphism short of a actual type system is going to cause this problem I think. Where does that leave us (me?)? Assuming the above I think then that the right answer to me is to have a
which either compiles to something like:
or better is entirely invisible outside of the scope of a class or it's sub-classes. I think Damian concerns would be mitigated if is only be declarable on methods, but I can foresee a world with a This would allow providing some way to communicate some private state in a controlled fashion without opening the world to public consumption of our implementation details. Footnotes |
Having spent more time thinking and reading: alternatively we could just enable shadowing of parent fields.
We get 0 behavior re-use but we have a much more solid contract with behavior than anything else I can think of. |
Agreed that method execute () {
$self->one;
$self->two;
$self->three;
}
method two () :trusted { ... } In the above, |
The RFC as it stands does not give enough clarity on the scoping nature of field names. In particular, all of the examples simply use unique
field ...
names at the level of the class block itself, so they don't sufficiently explain various cornercases.Lets for now entirely ignore the generated method names for
:reader
or:writer
attributes, or the constructor behaviour of a:param
, and think purely about the field as an internal storage mechanism.I can imagine any of several different models.
1.
my
-likeSince already we're saying that fields are private within a given class; that methods even in a directly derived subclass cannot see them, I wonder if they are lexically scoped within the block that declares them. I could see a model in which they scope the same as
my
variables. Compare the analogy of:This has a notable upside, in that if an individual method wanted to do some sort of per-instance memoization, it can hide a field variable in its own little block to act as a storage cache; in a similar way to the way regular
sub
s can:That variable is now hidden from all the other methods, and name clashes don't matter.
I like this model because it means that these fields (names beginning with
$
or other sigils) scope the same way as lexical variables. Similar looking things behave similarly.2.
method
-likeAlternatively, I could imagine that the set of field names declared on a class is visible once throughout the class. By analogy to how package subs are callable even between physically-separate parts of the same package, I could imagine that fields are similarly visible:
In this arrangement, it doesn't matter what block-level scope a field is declared in. Once created for a given class it now exists, just once, in any code that's part of that class.
I don't like this model because it makes these fields scope the same as methods, even though they look superficially like lexicals. It's a subtle and complex model to try to explain to people.
3. Some other model?
There may be some other ideas, but so far I can't think of another good one.
The text was updated successfully, but these errors were encountered: