-
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
Access to constructor params from within ADJUST and field-initialiser blocks #77
Comments
Latest thoughts: While it doesn't help the field-initialiser block, an ADJUST (%params) { say join "|", keys %params }
ADJUST (%args) { say "Foo is $args{foo}" }
ADJUST { say "params aren't visible here at all" } which would allow ADJUST blocks to request access to the params, while neither eating up a reserved name nor using the |
I don't like the idea of "Special
Of the options presented so far, something declared by the user seems the only really acceptable option. As stated, it's not obvious how to combine that with initializer blocks, but I'm not sure that combining them is actually a necessary feature. |
Thinking further about it, I'm not sure I like the exact syntax of this latter approach. It's nice to imagine giving an ADJUST (%params) { say join "|", keys %params } Mostly because this really does something very different to the similar-looking syntax of method f (%params) { ... } What we're doing in the ADJUST block is really something a bit more like a refalias. Signatures don't support that yet, but if they did I imagine it'd look like Another potential downside to this current notation is that it doesn't leave us any room for adding more things in there. Again by analogy to slurpy parameters, it feels like I almost wonder if the ADJUST :please_provide_me_the_parameters_in_a_hash_called(%params) { ... } I'll take opinions on a nicer name ;) |
And now some more examples on the "declared metadata" front... Looking around at various actual For example, a few operate on a theme of taking the name of a file to be opened, Somewhat paraphrased: field $fh :param { undef };
ADJUSTPARAMS ( $params ) {
unless( $fh ) {
my $path = delete $params->{path};
# code here to open $path and store it in $fh
}
} It feels like this is a fairly common sort of pattern - parameters that are passed in, not to be stored directly, but instead to be used as part of a larger block of code that is evaluated at construction time which eventually yields the initial value for a field to be stored. It's annoying that these parameters aren't declared any how, and only work as a side-effect of running the actual code. There's no predelcared metadata, so things like name-clashes aren't visible like they are for
While it isn't yet a thing, it is tempting to imagine that perhaps some way to attach documentation snippets could be found for It's tempting to think that perhaps what is wanted is a new param path :apidoc(The path to the filesystem device to open, if 'fh' is not provided); However, that's just dangling out in space somewhere separate, there's nothing to attach it to the specific value within the It feels like the core of why this is hard to design comes from the fact that "named params" want to be two things at once:
There doesn't at the moment appear to be a syntax idea that somehow addresses both concerns. |
One possible answer to this: revisit the notion of "lexical fields" (at the very least still outside of methods) and through this, not needing to provide any parameters to ADJUST: Using the file example above:
|
I think we might revisit this by not looking at # called before new
around "BUILDARGS" => sub ( $orig, $class, @args ) { ... };
sub new ($class, $args) { ... } # implicit. We don't write this directly
# called after new
sub BUILD ($self, @args) { ... } For Corinna, I envisioned something vaguely like this (with naming suggested by @thoughtstream, with # before NEW
method CONSTRUCT :common (@args) {
# $class is available, but not $self
# must return even-sized list of key/value pairs that
# are passed to new()
}
method NEW (%args) { ... } # called implicitly using args returned by CONSTRUCT
# after NEW
method ADJUST (%args) {
# both $class and $self available. Do what you will before
# new object is returned
} This is somewhat described here in the Wiki, but I see now that it didn't make it into the official RFC. When I saw you were using
However, the Moo/se paradigm is only awkward (in my opinion) because Moo/se is limited by Perl's current syntax. The overall idea seems solid. |
Your suggestion there doesn't seem to any clearer answer how to handle these different params. Perhaps a concrete example is needed. Lets imagine some UI widget that needs a title and a colour specified in red/green/blue components. The title is just stored directly as text, but the red/green/blue are actually forwarded to the constructor of a "Colour" object, which is then stored. Right now in class Widget {
field $title :param;
field $colour;
ADJUST ($params) {
$colour = Colour->new( delete %{$params}{qw( red green blue )} );
}
}
my $widget = Widget->new( title => "The text", red => 100, green => 30, blue => 0 ); This has some nice properties:
E.g. consider what happens to someone who does It has some downsides to it though. Most notably, the words class Widget {
field $title :param :is(Text) :apidoc("The title text to display at the top");
....
my $widget = Widget->new( title => ... If the user hovers the mouse over Where in the With reference to your suggested methods above ( |
I'm still not seeing the problem. Currently, this can be done in Moose with
So in the latter case: class Widget {
field $title :param;
field ($red, $greed, $blue ) :param;
field $colour;
ADJUST ($params) {
$colour = Colour->new( delete %{$params}{qw( red green blue )} );
}
}
my $widget = Widget->new( title => "The text", red => 100, green => 30, blue => 0 ); Note that the colors don't have readers or writers, as per your original code, so I think this satisfies that and it's more correct because the user must supply them. (You can always return If my code sample doesn't address your issue, can you help me understand why? If this is solvable under Moose but not Corinna, can you show me a Moose example? |
The issue with your code example is that the "fields" As for the comparison with Here's an example which can be made to work with Moose, but not with Object::Pad: use Object::Pad;
class Complex {
field $real :param;
field $imaginary :param;
}
class Real :isa(Complex) {
ADJUST {
# Here, I should be able to set $imaginary to 0. But how?
# $imaginary isn't available, and setting imaginary => 0
# has no place to set it
}
}
my $real = Real->new(real => 123);
# Required parameter 'imaginary' is missing for Complex constructor |
It could be considered a major flaw. Right now in this particular example they're just numbers being the colour components; so it's a fairly harmless problem. But consider if those were large object references themselves. Those dead fields needlessly holding them are going to inflate the refcount, delaying their destruction. That at least wastes memory and depending on what other behaviours might be attached to destruction time of those objects, might actually be a bug, if some other code was relying on timely destruction of them. We really mustn't store construction-time parameters as fields unless we intend to store them as fields. Using the fields just as temporary storage during construction time and then not touching them afterwards isn't really a good solution. |
@HaraldJoerg mostly explained that in the reply; I expanded on it above.
Well I don't know Moose anywhere near enough to suggest how to do it there, but consider this in classical perl: package Widget;
sub new ($class, %params) {
my $title = delete $params{title};
my $colour = Colour->new( delete %params{qw( red green blue )} );
die "Unrecognised construction parameters - " . join(", ", sort keys %params) if keys %params;
my $self = bless {
title => $title,
colour => $colour,
}, $class;
return $self;
} We haven't pointlessly stored the plain |
Ah, that example is showing off an equally-valid, yet quite different problem. That's more about how subclasses can alter the definitions of existing fields on their parent classes. It's quite common in Moose for example to alter an existing field; in your example case: has '+imaginary' => default => sub { 0 }; The For your usecase, it's as if we'd want to allow something like: class Complex {
field $re :param :reader :writer;
field $im :param :reader :writer;
...
}
class Real :isa(Complex) {
inherited field $im { 0 }; # give it a default value
} though ideally you'd want to try to remove the ":param" part of it; making it a construction-time error to explicitly pass it. However, that being the case it does sortof suggest at least this example, saying that a Real number is a subclass of a Complex one, is perhaps not the best example. What should be the correct behaviour of
? |
my $z = Real->new( re => 4, im => 3 ); That should be an error. I could live with a sloppy implementation where I might be able to come up with a more complex, but more relevant example at some time. I can not quote any relevant literature on that, but in my belief a class is responsible for the complete API of its constructor. Saying "ah, and in addition you can pass all parameters of the base class" is frequent practice and sort of acceptable if you don't want to duplicate documentation, but not correct. The fact that class So, some instance within class |
Now I see your concern, but I don't see it as a "major flaw" in terms of Corinna. It's a major flaw in terms of the general expressiveness of most OO systems. With those systems that define a constructor as the name of the class, you could do this (pseudo-code):
For those types of OO systems (e.g., Java), no With current Corinna syntax, it can still be fixed (keeping in mind that class Widget {
field $title :param;
field ($red, $green, $blue ) :param;
field $colour;
ADJUST ($params) {
$colour = Colour->new( delete %{$params}{qw( red green blue )} );
undef $_ foreach $red, $green, $blue;
}
} In the above, we get undefine those values, but the metadata is still available. That solves the issue, yes? I think (I could be wrong) that this is an edge case that's less likely to be a serious issue, but it's easy to solve if it arrives. I acknowledge that you might write code that often hits this issue. I'm unsure of the last time that I have. However, I do admit that I've written plenty of code where arguments are required for construction, but not post-construction. |
Well, that's annoying 😃 I'm not sure of the best approach here. In classical Perl terms, the constructor for sub new ( $class, %args ) {
my @keys = keys %args;
if ( 1 == @keys && 're' eq $keys[0] ) {
return bless $class->SUPER::new(%args, im => 0), $class;
}
croak("...");
} In Moose, I think this would work: has '+im' => (
default => 0,
init_arg => undef,
); You'd probably want So for Corinna, possibly something like this? class Complex {
field $re :param :reader :writer;
field $im :param :reader :writer;
...
}
class Real :isa(Complex) {
field $im :overrides { 0 }; # give it a default value
} If we wanted Or we could say that |
I could argue that the pure existence of the writer makes the class unsuitable for subclassing with a class where the value is supposed to stay fixed. As the author of a subclass I have to check the complete contract of the parent class. If I want to subclass anyway, I have an explicit way to express that. Depending on the actual situation, either of these implementations might be appropriate: method set_im :override ($whatever) {
die "Reality alert: You promised this would not happen for this Real object.";
} method set_im :override ($whatever) {
$whatever and warn "Enhanced reality mode active for this Real object."
} I think it is totally acceptable if cases like this can not be solved in a declarative way. But also: We digress :) The auto-generation of readers and writers is a really nice convenience feature, but unrelated to object construction. |
The Consider, for example, this implementation of Complex and Real:
To make this work would require changing the specification to say that only the top-most class's CONSTRUCT phase is called. (Note that Complex's CONSTRUCT dies. This means Real's attempt to rewrite could only work if Real is the first (and possibly only) class to receive CONSTRUCT.) That phase is responsible for making the named argument list look like whatever it needs to look like for the benefit of itself and also its superclass(es) and role(s). To that end, it should have the option of being able to explicitly call superclass/role CONSTRUCT phases directly, and dispensing with the returned argument list as it sees fit (even as far as saying it's on the caller to deal with an odd-sized list). E.g. (The idea that a phase called Speaking of, I mentioned previously the idea that we could allow "constructor-only fields" (fields that may or may not be |
I'm planning a much (much much) longer response to @leonerd's original question, But, for now, just a quick word about Unless the classes in question produce immutable objects, these are the classic examples Note, however, that the solution to @leonerd's original question that I will be proposing |
Unsurprisingly, I have observations and comments on this issue. Some preliminary thoughts
Design requirementsGiven all of the above, here’s my list of design requirements
DesignMy thinking for this design was as follows: If all initialization options So far, in Corinna, we’ve done that by the clever trick of using So why not use the same trick in reverse? Why not associate specific In other words: if field initializer blocks and Better still, those parameter names would also declare The only problem is that, in current Perl syntax, So we simply change that reality... A. Pre-tweaking constructor arguments
B. Field initialization via
|
Agreed, but this could be done:
That would automatically be a class method and would be expected to return an instance of something. This could be useful for factory classes where it's clear that what is returned is not an instance of That being said, I'm not a fan of a proliferation of different ways of naming constructors. The multi-method approach you describe seems warranted. I'm also not a fan of the name |
Yes, I think there's much to be said for avoiding the potential Babel of:
Or, to put it another way, a
Arguably, this phaser implements a user-redefinable interface for the signature of But none of these really ”sparks“ for me, so I left it at |
Wow, much good thought here I see. I'll respond to a few quick bits to get them out of my head but I will write up a longer response later too. All of @thoughtstream's "Preliminary Thoughts" look good. Nothing I disagree with there. Of the design itself: It's far from a "no", but I am slightly cautious about trying to add too much meaning to the names of signature parameters. Not so much because it's a bad idea here, but simply that if we do it here we should consider wider Perl language overall and what it might mean to do similar things elsewhere. It may well be something we decide we want to do elsewhere too, but if we do we should aim to make something consistent across the whole language. I could easily live with |
@leonerd wrote:
Caution is always a wise starting position. :-)
The consistent thing (that we’re having to work around here, because Perl doesn’t have it) In Raku we can say:
...and each value ends up bound the the correct parameter, despite That’s effectively what I’m suggesting for Of course, it would be vastly better if this didn’t need to be a special case. If Perl already had named parameter binding, then the solutions
In which case those “match the named constructor arg to the identically named adjuster parameter”
That, of course, is the other thing I was trying to work around in my proposal: If Perl already supported multimethods, we wouldn’t need Then, if class designers wanted to support alternative constructor signatures,
(Though, to be honest, the Alas, Perl has neither “bound-by-name” parameters, nor multimethods. But given the improbability of convincing the Powers That Be to add either |
At the risk of being controversial by not being cautious, @leonerd has written Syntax::Keyword::Multsub which gives us multiple dispatch. There are a couple of bugs which need to be resolved, but those don't look insurmountable. I would, however, prefer sticking to the KIM syntax because method new :multi ($num) { ... } Evolving Perl to stick with this syntax will make it more consistent and predictable over time. I suspect this would also make Paul's proposal for a metaprogramming API a touch easier. |
True, but multi-method constructors seem (to me) to be easier to understand because they omit some of the complexity and having |
@Ovid observed:
My understanding is that BTW, ever since a much earlier private discussion with @Ovid on the need for In contrast to @leonerd’s wisely cautious and elegantly minimalist approach, It is a pure Perl implementation, so it runs appreciably slower that @leonerd’s (Not entirely) coincidentally, I had been planning to unleash this module on an unsuspecting Perl world However, despite all that, I am not really convinced that either
I can’t speak to the meta-API issue, and despite my previous championing of KIM syntax, Multisubs and multimethods are fundamentally different entities from subs and methods, In short, they are sufficiently distinct that I believe they need a separate keyword. But, once again, I fear we are straying from the main issue here.
Agreed. Though they also thereby omit some of the safety and robustness and
This to me is the clincher. Multiple dispatch is indeed tremendously useful However, I fear that designing, marketing, selling, and then implementing Hence, I suspect it will be some considerable time, if ever, before And, with the most genuinely profound respect to @leonerd Happily, in this particular case, multimethods are not actually required. Instead, we can mandate that the constructor argument API for Corinna Either via a singly dispatched
...or else via some third-party MD solution (such as
|
I'm actually OK with that and saying it's out-of-scope for the MVP. I don't particularly see a problem with saying, "we have a consistent syntax and you have to use it for now." I'd rather we see how simple we can make things for the MVP and verify that the overall thing works. Once we release, we can see how it plays out in the real world. (though this means people are going to write I suspect that this might be a minority opinion. |
+1 from me |
The class Complex {
field $re :param :reader;
field $im :param :reader;
}
class Real :isa(Complex) {
NEWARGS (%args) { die if exists $args{im}; (%args, im=>0); }
} This is nice, and the only downside is that there's no declaration to show that role Appearance {
field $colour :param;
}
class Widget :does(Appearance) {
field $title :param;
NEWARGS (%args) {
(title => $args{title},
colour => Colour->new(@args{qw(red green blue)}),
)
}
}
my $widget = Widget->new(title => "The text", red => 100, green => 30, blue => 0 ); This works, but as in @leonerd's initial example:
...only this time it is in The obvious solution - |
@HaraldJoerg observed:
Setting aside the crimes against Liskov Substitutability (;-),
Again, an S.A.M.D.T. can restrict named argument lists
|
As an aside, I see an issue with this: class Widget {
multimethod new (%{ title=>$t, red=>$r, green=>$g, blue=>$b}) { ... }
multimethod new (%{ title=>$t, color=>$c }) { ... }
} In particular, maintainability. There's nothing to stop someone from writing this: class Widget {
multimethod new (%{ title=>$t, red=>$r, green=>$g, blue=>$b}) { ... }
# thousands of lines of code
multimethod new (%{ title=>$t, color=>$c }) { ... }
} And then the poor maintenance programmer (often me, damn it), is unaware that there's something else they have to think about when maintaining the code. There's an old joke about doctors where the patient complains that "it hurts when I do X" and the doctor replies "then stop doing X." The Perl developer response to my concern is typically "then don't do X." There's a certain merit to this. When the software can protect us, it should. In this case, we have things that are semantically related, but that's not expressed in the code. So instead, I've long thought I'd prefer something like this (abominable) syntax: class Widget {
multimethod new {
args (title=>$t, red=>$r, green=>$g, blue=>$b) { ... }
args (title=>$t, color=>$c) { ... }
args ($title) { ... }
}
} With the above, developers wouldn't be grouping multis by happenstance. They'd be doing so because the syntax forces them to be more organized, making the code easier for others to develop. |
Hmmmmmmmmmmmmm. Given the astonishing advances we’re currently seeing in machine learning use reasonable 'semantics';
class Car :isa(Wheel) {...}
# Compile-time error: Category mistake (a car is not a wheel)
class Real :isa(Complex) {...}
class Square :isa(Rectangle) {...}
# Compile-time error: Liskov violations
class Utils :does(Maths) :does(Stats) :does(Transforms) {...}
# Compile-time warning: Possible "God object" detected Hmmmmmmmmmmmm.
That’s extremely interesting. I don’t recall having seen that particular approach However, although I certainly agree this kind of restriction For example, imagine a multi-based implementation of some
When someone creates a new class, and would like the
And now the client code’s More relevantly, however, is the fact that, just like regular methods, class MyBase {
multimethod do_something ($x, $y) {...}
}
# thousands of lines of code
# or in a completely separate source file
role Something {
multimethod do_something ($z) {...}
}
# thousands more lines of code
# or in another completely separate source file
class MyDer :isa(MyBase) :does(Something) {
multimethod do_something ($x, $y, $z) {...}
} This has to be supported, no matter what, so if you attempted to impose Nevertheless, I can see the desirablity of encouraging developers Thank-you for the idea. [Obligitory "We're straying from the topic again" reminder...ironically, by the main offender] |
In that case, it seems we're down to three options (someone correct me if I'm wrong)
I don't think we're getting multidispatch soon, so scratch that. The "do nothing" raises the question: does this create an unsolveable problem? I'm not convinced that it does, and for some workarounds, how likely are they to be in practice? That being said, "do nothing", while sounding tempting, doesn't seem like the best approach to me because at the very least, we need |
I have been musing for some time now whether it is possible to merge the Rationale: Both attributes describe part of how a field gets its initial value. Here are some examples: field $name :param;
field $name :param(name); # same behavior as previous line The caller must pass a value for field $name :param(label) The caller must pass a value for Now let's add initializer blocks: field $name :param { $name // 'N.N.' } # default is 'N.N.'
field $name :param { $name } # defaults to undef
field $name :param(label) { $label } # label is optional, defaults to undef This is a change to the current spec (and to the behavior of Object::Pad): The caller may pass a value So one example we had before could be written like this:
Both The widget example: class Widget {
field $title :param;
field $colour :param(red,green,blue) { Colour->new( $red,$greem.$blue ) };
}
my $widget = Widget->new( title => "The text", red => 100, green => 30, blue => 0 ); Like in @leonerd's initial example, it is not possible to specify a The fact that initializer blocks are always run has some interesting consequences. class Breakfast {
has $drink :param { Coffee->new; }
} This is no longer specifying a default. Instead, it ignores whatever parameter has been given to the constructor. As another example, array fields can be initialized from constructor parameters like this: class Polygon {
field @points :param { @$points }
} Also, initializer blocks can now be used to validate the input, which is kinda nice because it happens early in the construction process and in many cases there will be a 1:1 relationship between a param and a field. A construction parameter can be used to build more than one field: class Prism {
field $height :param { $height // 1.0 }
field $size :param { $size // 1.0 }
field @vertices :param(sides,size,height) { ...; }
field @faces :param(sides) { ...; }
}
my $p = Prism->new(sides => 6) # create a hexagonal prism There's still a need for |
I think @HaraldJoerg’s “initializer-blocks-always-run” proposal Most importantly, the current behaviour of initializer blocks For example: field $colour :param :adjust($r=0.5, $g=0.5, $b=0.5) { Colour->new(r=>$r, g=>$g, b=>$b) } ...allows users to either pass a I can’t really see how to achieve that under @HaraldJoerg’s proposal. And here's an even simpler example: # Initialize from the 'name' NCA, or else from the 'label' NCA...
field $name :param :adjust($label) { $label } Moreover, this example tells the compiler to mark the The issue here is that I would also point out that my previous proposal already allows for the field $name :param { 'N.N.' } # default is 'N.N.'
field $name :param { undef } # defaults to undef
field $name :param(label) { undef } # label is optional, defaults to undef # If no 'title' NCA, use 'label' NCA instead...
field $title :param :adjust($label) { ucfirst lc $label } class Widget {
field $title :param;
# Must pass 'red', 'green', and 'blue' NCAs; can't pass 'colour' NCA...
field $colour :adjust(red,green,blue) { Colour->new($red,$green,$blue) };
} class Breakfast {
has $drink :param { Coffee->new; } # Now not an error (Coffee is the default)
# or...
has $drink { Coffee->new; } # Also not an error (Coffee is mandatory...and obviously so)
} class Polygon {
field @points :adjust($points) { @$points }
} class Prism {
field $height :param { 1.0 }
field $size :param { 1.0 }
field @vertices :param :adjust(sides) { ...; }
field @faces :param :adjust(sides) { ...; }
}
my $p = Prism->new(sides => 6) # create a hexagonal prism In summary: I completely understand (and approve of) the desire to However, I don't believe that explicit initialization ( I specifically chose the name |
@thoughtstream says:
I omitted that intentionally (mumbling about
I can narrow down the difference in interpretation with @thoughtstream's quote:
In my interpretation,
|
But this doesn’t fully achieve the same effect. For a start, unlike my example, it doesn’t specify So then we’d start seeing: field $colour :param(colour,r=0.5,g=0.5,b=0.5) {
die "Can't use 'colour' and 'r'/'g'/'b' at the same time"
if defined $colour && grep {defined}, $r, $g, $b);
$colour // Colour->new(r=>$r, g=>$g, b=>$b)
} More generally, the behaviour of the field $colour :param :adjust($r=0.5, $g=0.5, $b=0.5) { Colour->new(r=>$r, g=>$g, b=>$b) } is maximally declarative. The only code required is the code that provides the actual default/fallback value. field $colour :param(colour,r=0.5,g=0.5,b=0.5) { $colour // Colour->new(r=>$r, g=>$g, b=>$b) } is emergent. The default/fallback only occurs because of the Which means it’s much easier to enbug that version: field $colour :param(colour,r=0.5,g=0.5,b=0.5) { $colour || Colour->new(r=>$r, g=>$g, b=>$b) } The same issues apply even more to the alternative formulation @HaraldJoerg suggested: field $colour :param(colour,r,g,b) { $colour // Colour->new(r=>$r//0.5, g=>$g//0.5, b=>$b//0.5) } This has all the same non-declarative disadvantages as the previous version but, in addition, And the possibility for unintended effects is even greater: field $colour :param(colour,r,g,b) { $colour || Colour->new(r=>$r//0.5, g=>$g//0,5, b=>$b/0.5) }
I’m arguing that we do need that distinction. In order to generate correct error messages. |
I'm starting to think we're looking at putting WAY too much into a :param attribute, especially for something that needs an MVP implementation. What started as a simple "put this NCA into a field for me" is looking like it's being slowly turned into a field-by-field subroutine signature. I'm going to once more reiterate my own proposal for this solution space.
The idea here is to achieve declarative NCAs yet keep things simple enough to be possible for an MVP. Since our colour example keeps coming around, that results in:
Item 3 has the goal of allowing derived classes to have absolute control over the initialization of their superclasses. This was also the motivation behind item 5 (to allow the derived class to delegate unknown NCAs to base class). If a base class or role isn't given a ->new() call by the subclass, the overall construction logic should automatically construct the base and roles as if they had no parameters. |
(Just to re-iterae something: the "fields not used beyond construction can be freed" would extend to ANY field,
) |
I agree with @aquanight that my I'd be fine if the first release of Corinna has only simple But this discussion is also about what Corinna should provide long-term, as it evolves. That said, if you truly believe that: field $with_rgb { 1 };
field $r :param { $with_rgb = undef; };
field $g :param { $with_rgb = undef; };
field $b :param { $with_rgb = undef; };
field $with_colour { 1 };
field $colour :param { $with_colour = undef; $with_rgb or die; Colour->new(r => $r, g => $g, b => $b); };
ADJUST { !$with_rgb != !$with_colour or die; } ...is an adequate long-term alternative to something like: field $colour :param :adjust($r, $g, $b) { Colour->new(r => $r, g => $g, b => $b) }; ...or perhaps: NEWARGS ($r, $g, $b) { colour => Colour->new(r => $r, g => $g, b => $b) };
field $colour :param; ...then I suspect our criteria for programming language design are just too fundamentally different Mind you, I'm not saying which of us is right; just that one design approach must die |
Current state in Object::Pad
First, a little background on the current state of
Object::Pad
. There areADJUST
blocks, and there are field-initialiser blocks. They behave in very different ways.The
ADJUST
keyword creates an entire (anonymous) function which behaves as such when invoked. It is passed a hashref to the constructor params as its first argument. It can access that with$_[0]
orshift
or optionally by providing a signatured parameter name:(Of course, these examples are stupid in that they're doing things that
:param
would be far better suited to, but I didn't want to cloud up the examples with more complex real-world examples.)By comparison, field-initialiser blocks are simple blocks, being parsed by no small amount of parser trickery into believing they're all just sequential blocks of the same function. That function is a single (anonymous) function stored as part of the class, used to initialise all the fields. These initialiser blocks do not currently have any access to the constructor parameters; which is somewhat limiting.
Both ADJUST and field-initialiser blocks get access to a
$self
lexical, the same as other methods.Current state in feature-class branch
The current
feature-class
branch providesADJUST
blocks that allow either of the first two forms, but not the third form (with a signature parameter). The branch does not currently provide field-initialiser blocks at all.The Question
And now we get on to my question. I would like to unify these two things together and at the same time make them better. In particular, I would like both ADJUST and field-initialiser to be simple blocks within one (implied) function rather than having one entersub overhead for every block. Additionally, I would like all of them to have the same access to the constructor parameters.
In particular, it would be great if we could say that a field-initialiser block is really just the same as an ADJUST block that assigns the field from its result, give or take some code that inspects the parameters hash to see if the caller already provided a value.
The trouble though is how to provide this params hash(ref). It can't just come as the first value in
@_
because any block would too easily be able to break access to it for all the later ones by just doingshift
, or something equally dumb. It needs to be provided by something guaranteed to be visible to all the blocks, that no earlier block can get in the way of. So far I can only think of two ideas, neither of them are great:Special
%params
lexicalIn the same way that
$self
is special, make%params
special in these blocks:It's a simple idea and easily understandable, but it does mean that no class is permitted a field called
%params
itself, because then there'd be no way for an ADJUST block to see it. Of course we're already in that situation at the moment with$self
, so it doesn't make the problem much worse. But perhaps enough classes might themselves want afield %params
, that this would become awkward.Use the
%_
superglobalBy considering analogy to the
@_
superglobal already in perl, we can consider using the%_
hash as a storage of these name/value pairs:I think there's a certain neatness to this, and a certain symmetry with using
$_[0]
,$_[1]
, etc... However, some folks might object to it on grounds that "newbies to perl might be confused by lots of symbol syntax". Personally I'm not very swayed by this particular argument, but it seems important to some. Another potential argument against doing this is that it seems weird to introduce a new use for%_
while at the same time trying to get rid of@_
in favour of function signatures.Other Designs
Another way to look at it entirely, is to observe that the main reason for wanting the hash(ref) of params available in these blocks in the first place, was to do things with constructor parameters that aren't just simply "copy the value into a field". This was originally discussed for
Object::Pad
in RT137209. I say "discussed" - I thought out loud on a few ideas but nobody else has commented so far.In many ways the current solution of just passing a hash(ref) around isn't very nice. It means that the actual parameter names used by the construction process overall are not manifestly expressed anywhere, and only become apparent in side-effects of the actual process of constructing an object. You can only find out those names by running it and seeing if it complains about missing or extra ones. It would be nice to find a nicer overall design for this sort of pattern, but so far one has not emerged.
The text was updated successfully, but these errors were encountered: