Skip Menu |

This queue is for tickets about the Moose CPAN distribution.

Report information
The Basics
Id: 51486
Status: rejected
Priority: 0/
Queue: Moose

People
Owner: stevan.little [...] gmail.com
Requestors: mschwern [...] cpan.org
Cc:
AdminCc:

Bug Information
Severity: Important
Broken in: 0.92
Fixed in: (no value)



Subject: defaults are applied before all args from new() are assigned
It seems like attribute defaults are run before all the arguments from new() are set in the object. This causes problems if a default references another attribute, it won't yet be populated. The attached test demonstrates. A work around is to set the attribute as lazy, but this behavior is Surprising.
Subject: bugs.plx
Download bugs.plx
application/octet-stream 765b

Message body not shown because it is not plain text.

Subject: Re: [rt.cpan.org #51486] defaults are applied before all args from new() are assigned
Date: Fri, 13 Nov 2009 01:30:51 -0600 (CST)
To: Michael G Schwern via RT <bug-Moose [...] rt.cpan.org>
From: Dave Rolsky <autarch [...] urth.org>
On Fri, 13 Nov 2009, Michael G Schwern via RT wrote: Show quoted text
> It seems like attribute defaults are run before all the arguments from > new() are set in the object. This causes problems if a default > references another attribute, it won't yet be populated. The attached > test demonstrates.
Actually, you left a "lazy => 1" that makes your test pass. Show quoted text
> A work around is to set the attribute as lazy, but this behavior is > Surprising.
The manual does mention this gotcha in several places and specifically says: When the "default" is called during object construction, it may be called before other attributes have been set. If your default is dependent on other parts of the object's state, you can make the attribute "lazy". -dave /*============================================================ http://VegGuide.org http://blog.urth.org Your guide to all that's veg House Absolute(ly Pointless) ============================================================*/
As Dave pointed out, this gotcha is mentioned in the manual. The order of attribute initialization is hash order and therefore not to be relied upon. If you need control over the ordering use lazy. Sorry this is surprising to you, but it is not a bug, it is documented behavior. - Stevan
Quoted from Moose::Meta::Attribute docs: "Note that there is no guarantee that attributes are initialized in any particular order, so you cannot rely on the value of some other attribute when generating the default." FWIW, I would be willing to accept a patch to make this more explicit in the other docs so as to not be as surprising.
On Fri Nov 13 07:01:29 2009, STEVAN wrote: Show quoted text
> Quoted from Moose::Meta::Attribute docs: > > "Note that there is no guarantee that attributes are initialized in > any particular order, so you cannot rely on the value of some other > attribute when generating the default."
I don't see that in the 0.92 docs. Ahh, its in Class::MOP::Attribute. Of course. :/ Show quoted text
> FWIW, I would be willing to accept a patch to make this more explicit > in the other docs so as to not be as surprising.
Instead of putting bigger signs around the pit, could we fill in the pit? Is there a good reason defaults aren't applied last?
On Fri Nov 13 19:45:50 2009, MSCHWERN wrote: Show quoted text
> On Fri Nov 13 07:01:29 2009, STEVAN wrote:
> > Quoted from Moose::Meta::Attribute docs: > > > > "Note that there is no guarantee that attributes are initialized in > > any particular order, so you cannot rely on the value of some other > > attribute when generating the default."
> > I don't see that in the 0.92 docs. Ahh, its in Class::MOP::Attribute. > Of course. :/
Sorry, yes you are correct. Show quoted text
> > FWIW, I would be willing to accept a patch to make this more explicit > > in the other docs so as to not be as surprising.
> > Instead of putting bigger signs around the pit, could we fill in the > pit?
Honestly, it is a pit people fall in once, perhaps twice, but probably no more then a handful of times before they learn that if you need control over this, you use lazy. We really try hard to balance between trying to not surprise new users and making sure advanced users are not hampered. You may not agree with the choices we make regarding this, and that is your right, but know that they were not made hastily and often times were pondered over days, weeks, months and even years. Show quoted text
> Is there a good reason defaults aren't applied last?
Because that doesn't solve the problem, what if the other attribute you want to call also has a default? Trust me, we have thought this one through pretty extensively and we even had a discussion again about it this morning after I saw your bug. Trying to put a definitive and predictable order on attribute initialization is pretty hairy in the face of things like lazy attributes, roles, multiple inheritance, attribute overriding (+foo) and such. Sure it could be figured out, but we pretty much all agreed that the additional code and probable performance penalty would not be worth it when lazy exists and works exceedingly well in this capacity. Personally, I have a real aversion to having multiple features which are mostly similar but behave in subtly different ways. Which is pretty much what we would end up with since we almost certainly need some kind of lazy-like feature contained within the attribute initialization phase which would behave in subtly different ways from the real lazy. I find these types of things to be conceptual pits (to borrow you analogy) that are far more difficult for users to navigate and understand then something like "when you need control over the ordering, just use lazy". All this said, I will leave this bug open as a reminder for us to include better docs that explain the lack of init ordering and reasons why 'lazy' is the appropriate solution to that problem. Of course I would gladly accept a patch from you for this if you are so inclined. If you are still interested in having this type of feature I suggest you contact Jesse Luehrs (doy on IRC) or Yuval Kogman (nothingmuch on IRC) as they both spent some time pondering a possible MooseX:: module for this based on attribute traits, I am sure they would be happy to pass on their ideas to you. - Stevan
Subject: Re: [rt.cpan.org #51486] defaults are applied before all args from new() are assigned
Date: Sat, 14 Nov 2009 10:18:27 -0800
To: bug-Moose [...] rt.cpan.org
From: Michael G Schwern <schwern [...] pobox.com>
Stevan Little via RT wrote: Show quoted text
>> Is there a good reason defaults aren't applied last?
> > Because that doesn't solve the problem, what if the other attribute you want to call also has a default?
If A depends on B and B depends on A and neither are supplied then they're actually circular, its impossible to resolve and that's the user's fault. This would not be a surprise, at least not Moose's, as it would be a logic error with or without Moose. Show quoted text
> Trust me, we have thought this > one through pretty extensively and we even had a discussion again about it this morning after I saw your bug. Trying to put a definitive > and predictable order on attribute initialization is pretty hairy in the face of things like lazy attributes, roles, multiple inheritance, > attribute overriding (+foo) and such. Sure it could be figured out, but we pretty much all agreed that the additional code and probable > performance penalty would not be worth it when lazy exists and works exceedingly well in this capacity. > > Personally, I have a real aversion to having multiple features which are mostly similar but behave in subtly different ways. Which is pretty > much what we would end up with since we almost certainly need some kind of lazy-like feature contained within the attribute > initialization phase which would behave in subtly different ways from the real lazy. I find these types of things to be conceptual pits (to > borrow you analogy) that are far more difficult for users to navigate and understand then something like "when you need control over the > ordering, just use lazy".
I can't argue about the details of Moose, but "defaults are applied last" doesn't seem like a conceptual pit. Since initializing an object is an atomic operation from the user's perspective, when default is called within initialization isn't even under consideration. There seems two very easy ways to handle it. When you loop over the attributes initializing the object, do the user supplied keys first. Or have two attribute initialization loops, the first does everything BUT defaults and the second applies defaults. The former seems like its just a small change to Class::MOP::Class->_construct_instance(). It also seems like the least exceptional thing to do. I've attached a small patch which fixes the problem. All Class::MOP and Moose tests pass without change. It makes my bug test pass. I'll whip up some Class::MOP tests if you want to take the patch. It can be optimized to be O(n) instead of O(n log n) if its thought that's a realistic performance problem. -- 3. Not allowed to threaten anyone with black magic. -- The 213 Things Skippy Is No Longer Allowed To Do In The U.S. Army http://skippyslist.com/list/
diff --git a/lib/Class/MOP/Class.pm b/lib/Class/MOP/Class.pm index 9e969ee..9f6c20d 100644 --- a/lib/Class/MOP/Class.pm +++ b/lib/Class/MOP/Class.pm @@ -361,7 +361,18 @@ sub _construct_instance { # but this is foreign inheritance, so we might # have to kludge it in the end. my $instance = $params->{__INSTANCE__} || $meta_instance->create_instance(); - foreach my $attr ($class->get_all_attributes()) { + + # Init the user supplied attributes first so defaults don't get applied + # before the attribute is set. + my @attributes = sort { + my $aexists = exists $params->{$a->name}; + my $bexists = exists $params->{$b->name}; + return 0 unless $aexists xor $bexists; + return -1 if $aexists; + return 1 if $bexists; + } $class->get_all_attributes(); + + foreach my $attr (@attributes) { $attr->initialize_instance_slot($meta_instance, $instance, $params); } # NOTE:
On Sat Nov 14 13:18:45 2009, schwern@pobox.com wrote: Show quoted text
> Stevan Little via RT wrote:
> >> Is there a good reason defaults aren't applied last?
> > > > Because that doesn't solve the problem, what if the other attribute
> you want to call also has a default? > > If A depends on B and B depends on A and neither are supplied then > they're > actually circular, its impossible to resolve and that's the user's > fault. > This would not be a surprise, at least not Moose's, as it would be a > logic > error with or without Moose.
Wrong example. What if A has a default and B has a default and neither are supplied and A depends on B? No circularity, but there's no way to know that B's default needs to be set before A's. Saying "let's just do defaults last" moves the problem instead of solving it.
Subject: Re: [rt.cpan.org #51486] defaults are applied before all args from new() are assigned
Date: Mon, 16 Nov 2009 22:53:22 -0800
To: bug-Moose [...] rt.cpan.org
From: Michael G Schwern <schwern [...] pobox.com>
Hans Dieter Pearcey via RT wrote: Show quoted text
> Wrong example. What if A has a default and B has a default and neither > are supplied and A depends on B? No circularity, but there's no way to > know that B's default needs to be set before A's.
Won't that auto-resolve? Let's play this out. # name depends on nothing has name => is => 'rw', isa => 'Str', default => sub { "Chuck" } ; # nickname depends on name has nickname => is => 'rw', isa => 'Str', default => sub { $_[0]->name } ; If nickname is called first, name is called, resolves to the default and nickname resolves fine. Then name is checked and its already resolved. If name is called first, name is resolved. Then nickname is called, it calls name which is already resolved, and nickname resolves. No problem, right? -- 151. The proper way to report to my Commander is "Specialist Schwarz, reporting as ordered, Sir" not "You can't prove a thing!" -- The 213 Things Skippy Is No Longer Allowed To Do In The U.S. Army http://skippyslist.com/list/
On Tue Nov 17 01:53:42 2009, schwern@pobox.com wrote: Show quoted text
> If nickname is called first, name is called, resolves to the default > and > nickname resolves fine. Then name is checked and its already > resolved.
Nope, this is the entire reason you have to mark things lazy to get them to work properly. When name is called, it won't automatically populate the default unless the name attribute is lazy - otherwise, the initial problem still occurs. -doy
Subject: Re: [rt.cpan.org #51486] defaults are applied before all args from new() are assigned
Date: Tue, 17 Nov 2009 01:14:44 -0800
To: bug-Moose [...] rt.cpan.org
From: Michael G Schwern <schwern [...] pobox.com>
Jesse Luehrs via RT wrote: Show quoted text
> <URL: https://rt.cpan.org/Ticket/Display.html?id=51486 > > > On Tue Nov 17 01:53:42 2009, schwern@pobox.com wrote:
>> If nickname is called first, name is called, resolves to the default >> and >> nickname resolves fine. Then name is checked and its already >> resolved.
> > Nope, this is the entire reason you have to mark things lazy to get them > to work properly. When name is called, it won't automatically populate > the default unless the name attribute is lazy - otherwise, the initial > problem still occurs.
So it doesn't DWIM. They're not part of the accessor but called separately during object instantiation? So its not safe to have a default depend on another default. :( I might as well take this to its logical conclusion. It seems like lazy solves a lot of problems. Why isn't lazy the default? Is there a drawback to lazy? I can imagine there might be a performance hit for checking if the default has been applied, and I see that Class::MOP::Method::Generated adds an exists check to the accessor for lazy. Benchmarking reveals a performance penalty of 30-40% on accessor reads, wow! So here is a situation where the default is faster, but unstable. Its too bad this isn't object inheritance where each object's accessor could be replaced with a slimmer one as soon as the default was applied. One solution would be during object instantiation to put into place full lazy/default style accessors and then remove them when instantiation is complete, but that would probably place a drag on object creation as well as introduce all sorts of fun side-effects. Ok, so full stability of non-lazy interdependent defaults without a performance hit cannot be achieved. My patch still solves a good chunk of the problem. It doesn't move the problem, but reveals a deeper, but narrower, problem that was there all along. -- 124. Two drink limit does not mean first and last. -- The 213 Things Skippy Is No Longer Allowed To Do In The U.S. Army http://skippyslist.com/list/
This is not a bug, you simply need to use (lazy => 1) and it will solve your problem, that is exactly the reason why that feature exists. Please use the mailing list next time, RT is not your personal Moose help-desk. - Stevan
Subject: Re: [rt.cpan.org #51486] defaults are applied before all args from new() are assigned
Date: Tue, 17 Nov 2009 15:48:09 -0800
To: bug-Moose [...] rt.cpan.org
From: Michael G Schwern <schwern [...] pobox.com>
Stevan Little via RT wrote: Show quoted text
> <URL: https://rt.cpan.org/Ticket/Display.html?id=51486 > > > This is not a bug, you simply need to use (lazy => 1) and it will solve your problem, that is > exactly the reason why that feature exists. Please use the mailing list next time, RT is not your > personal Moose help-desk.
Dude, I said from the beginning that setting lazy would "solve" it. If I just wanted to just make my code work I'd have left it at that. Users shouldn't have to set magic flags or study the documentation to make things work sensibly. They shouldn't have to set a make_it_work_right_but_a_little_slower flag (make_it_work_wrong_but_a_little_faster would be better). If you have to put caveats and warnings in the docs to steer the users away from the obvious choice of action that's a red flag and a source of user errors. I also like to report when I fall into pits so that later when someone else reports it they don't get "well you're the only one to report it so its not a problem". Bugs reports are like cockroaches, for every that gets reported there's a hundred you don't. Ten times that for interface problems which are technically not "bugs" and can be an uphill climb to convince the devs there's even a problem. I made the effort to dig into Class::MOP and figure out what's going on. I wrote my thinking out loud so maybe there could be a discussion or something. I submitted a simple patch which passes all tests and fixes a good chunk of the problem. I haven't been given a clear reason why it can't be applied as a partial solution. I didn't get an answer as to why lazy is not the default. If its for performance or just backwards compatibility, fine. Just say so. Until I know there's a good reason behind an awkward design choice I'll continue to pick at it. These are not personal help desk tickets. I found something hard to use, I want to make it easier for the next guy and a took a stab at patching it. That's what I do. That's what you're supposed to do. Not all bugs have an error message. -- 87. If the thought of something makes me giggle for longer than 15 seconds, I am to assume that I am not allowed to do it. -- The 213 Things Skippy Is No Longer Allowed To Do In The U.S. Army http://skippyslist.com/list/
Show quoted text
> Dude, I said from the beginning that setting lazy would "solve" it. If I just > wanted to just make my code work I'd have left it at that.
But that is the whole point, if you leave it out then your accessor will *not* be lazy and therefore will not exhibit the lazy behavior. We gave you a solution (actually you had it all along) that works perfectly and allows for room to grow even, but you decided you didn't want it, but wanted us to change core behavior instead. Show quoted text
> Users shouldn't have to set magic flags or study the documentation to make > things work sensibly. They shouldn't have to set a > make_it_work_right_but_a_little_slower flag
Well to start with, lazy is not a "magic flag", it is a flag that indicates the generator accessor should behave in a certain way which is pretty clearly explained in the docs and is really not all that hard to understand. It is one of those features that straddles the line and will do easy things easily (like your use case) and allows much more complex behaviors such as the Tree example in the Cookbook (which prevents infinite recursion in a circular data structure). As for users studying the documentation, I disagree, I think if you want to learn how to use a powerful tool, you are going to need to read some docs. It is simply impossible for a tool as powerful as Moose to be so intuitive to everyone that close study of the documentation is not required at some point. Show quoted text
> (make_it_work_wrong_but_a_little_faster would be better).
I could not possibly disagree more with that statement, making it "work wrong" but "a little faster" is not a compromise I am willing to take, I think my record on this is clear, I prefer correctness over speed anyday. And as with the "accessors should always be generated" discussion we had at YAPC, I would prefer that you get what you ask for, rather then having to ask *not* to get something. Also, your "right" and "wrong" are subjective and I disagree with your assessment, in fact I see them as the inverse. Show quoted text
> If you have to put caveats and warnings in the docs to steer the users away > from the obvious choice of action that's a red flag and a source of user errors.
I don't totally agree that it is "obvious" that default values will DWIM like you seem to think they should. But I think it has been pretty well established already that you and I disagree on what "DWIM" means so there is no need to belabor that. Show quoted text
> I also like to report when I fall into pits so that later when someone else > reports it they don't get "well you're the only one to report it so its not a > problem". Bugs reports are like cockroaches, for every that gets reported > there's a hundred you don't. Ten times that for interface problems which are > technically not "bugs" and can be an uphill climb to convince the devs there's > even a problem.
Sure, and we took your report to heart and it spawned several discussions among the cabal members, but in the end it was decided that lazy was a far superior solution. To make what you wanted work properly would not only increase the object construction and accessor overhead, but add complexity to a part of the code which is already pretty complex. To solve this problem correctly would require a partial laziness to be implemented within object construction which would not only complicate the part of Class::MOP you patched, but also all the constructor and accessor code generation in Class::MOP and Moose. The end decision was that all this work, overhead and future maintanance was not worth it when lazy already existed and does the job perfectly and, as I said before, allows for room to grow. Show quoted text
> I made the effort to dig into Class::MOP and figure out what's going on.
And I appreciate that very much. Show quoted text
> I wrote my thinking out loud so maybe there could be a discussion or something.
Right, but unfortunately you did it in an inappropriate forum, one which you have been asked repeatedly to not use in such a way. Discussion is for the mailing list or IRC where we can go over the details of the issue and get varied input from people /other then those who get the emails from RT/. If after that it is decided that it truely is a bug, then I am happy to have you put it into RT or re-open a closed or rejected bug. This is pretty much said/implied in the Moose::Manual::Contributing document as is the fact that we do not accept new features that are not first vetted by a MooseX:: module (which I suggested you do with this). Show quoted text
> I submitted a simple patch which passes all tests and fixes a good chunk of > the problem. I haven't been given a clear reason why it can't be applied as a > partial solution.
Actually, I disagree that it fixes the problem, or even a decent chunk of it, in fact I think that it creates more problems. Your patch solves your problem quite well, it does not solve the slightly more complex case Dieter (HDP) and Jesse (DOY) mentioned. Your partial solution would then require yet more caveats and workarounds to be added to the docs, something you yourself have said is a bad thing. Show quoted text
> I didn't get an answer as to why lazy is not the default. If its for > performance or just backwards compatibility, fine. Just say so.
Yes, it is exactly that ... and more. We have tried very hard to make sure (at runtime) you only pay for what you use and making lazy the default would incur a penalty even on those who didn't ask for it. But it is not just about performance penalty, I believe, as I said above, that you should let the user decide and not make decisions for them. I find it very unintuitive to have to ask to *not* have a feature. Show quoted text
> Until I know there's a good reason behind an awkward design choice I'll continue > to pick at it.
Again, we disagree on what DWIM means and what constitutes an "awkward design choice" so I don't expect to convince you. Show quoted text
> These are not personal help desk tickets. I found something hard to use, I > want to make it easier for the next guy and a took a stab at patching it. > That's what I do. That's what you're supposed to do.
Right, I don't disagree with that, I just think that RT is the wrong forum for *continuing* such a discussion, as I have stated to you already in the past (RT #34039 and #34313) and in my response to you in this ticket. Subscribe to the mailing list or visit us on #moose or even #moose-dev to discuss this further and pled your case. Show quoted text
> Not all bugs have an error message.
And not everything *you* think is a bug is /actually/ a bug. In this case, it is your opinion that Moose is doing the wrong thing, not an indisputable fact, and I flat out disagree with you. Honestly our fundemental disagreement reminds me of the whole debate over the RoR philosophy of "convention over configuration". You are suggesting that Moose embrace "convention", while I am much more firmly in the "configuration" camp. As I am sure you will agree, configuration is the more flexible of the two, but at the cost of requiring explicitness from the user. While convention brings more consistency and percieved simplicity, at the cost of reduced flexibility. Moose is not like other CPAN libraries, it is a library with which to write other libraries with. In essence Moose's problem domain is not unlike that of a programming language. And as we all well know, programming languages are very subjective things, some people love Perl and find it very intuitive (warts and all), others hate it with a strong passion. Moose has already clearly exhibited this exact same ability to evoke strong emotions and opinions in others. In fact, I have always said that Moose is not for everyone and that no one should ever feel compelled to use it if they don't want. So, anyway, in conclusion (cause this is getting long), please take these things to the mailing list or IRC where they can get greater feedback and discussion, but do not be surprised when you ask us to change core behavior that we don't insist you vet this behavior in a MooseX:: module first. There is nothing worse then adding a feature only to find it has inheriant problems and then being stuck in situation of having to maintain backwards compatibility for a mistake . - Stevan