Beefy Boxes and Bandwidth Generously Provided by pair Networks
Keep It Simple, Stupid
 
PerlMonks  

[Raku] Assigning defaults to attributes that are fixed length lists (and other confusions)

by tomgracey (Scribe)
on Apr 27, 2021 at 20:34 UTC ( #11131766=perlquestion: print w/replies, xml ) Need Help??

tomgracey has asked for the wisdom of the Perl Monks concerning the following question:

Hello Monks

After some consideration I've decided to continue posting my Raku questions on perlmonks. It is tempting to just go directly to the Raku IRC channel, however then the question/answer doesn't get recorded and thereby potentially help someone else out. When I was getting to grips with Perl I rarely needed to ask questions as I could nearly always find someone who had already posted a similar problem. With Raku searching doesn't seem to bring up so many Q and A type threads. No doubt much of this is due to it being so new, but I can't wondering if encouraging people to resolve problems via chat is actually that helpful. I don't know. Could be wrong. Just what immediately springs to mind. (Of course there is stackoverflow, but lets just say I am not such a great fan of their moderation methodology)

Anyway the show must go on, so here is my question:

Lets say I have the following (contrived) code:

class Hamper { has $.name = 'Christmas Basket'; has @.items is rw = 'Mince Pie', 'White Wine', 'Stinky Cheese' +; } my $hamper = Hamper.new; say "Name: " ~ $hamper.name; say "Items: " ~ $hamper.items;

This works as expected (that is I expect!), spitting out

Name: Christmas Basket Items: Mince Pie White Wine Stinky Cheese

However, say I want to limit the size of @!items to 3 elements. That is, I want the code to fall over with an error if I try to put 4 elements into it. And this is where I'm already not sure if I'm even going about it in the right way in the first place. My understanding (mentioned in the docs, and also e.g this post) is that I ought to be able to limit the size of the "positional" by changing the line which defines @.items to:

has @.items[3] is rw = 'Mince Pie', 'White Wine', 'Stinky Cheese';
The output is then:
===SORRY!=== Error while compiling /path/script.raku Defaults on compound attribute types not yet implemented. Sorry. Workaround: Create/Adapt TWEAK method in class Hamper, e.g: method TWEAK() { @!items := (initial values) unless @!items; } at /path/script.raku:3

I assume this to mean I created my fixed length positional successfully, but need to assign defaults separately because the compiler has not yet been trained on this particular scenario. Fair enough. So I do literally as the error message recommends, adding in the TWEAK method. My code now reads:

class Hamper { has $.name = 'Christmas Basket'; has @.items[3] is rw; method TWEAK(){ @!items := 'Mince Pie', 'White Wine', 'Stinky Cheese' unless @ +!items; } } my $hamper = Hamper.new; say "Name: " ~ $hamper.name; say "Items: " ~ $hamper.items;

Now the output is

Name: Christmas Basket Use of uninitialized value element[0] of type Any in string context. Methods .^name, .raku, .gist, or .say can be used to stringify it to s +omething meaningful. in block <unit> at script.raku line 16 Use of uninitialized value element[1] of type Any in string context. Methods .^name, .raku, .gist, or .say can be used to stringify it to s +omething meaningful. in block <unit> at script.raku line 16 Use of uninitialized value element[2] of type Any in string context. Methods .^name, .raku, .gist, or .say can be used to stringify it to s +omething meaningful. in block <unit> at script.raku line 16 Items:

Yuck! The defaults for @!items do not appear to get assigned. But if I remove unless @!items then I get:

Name: Christmas Basket Items: Mince Pie White Wine Stinky Cheese

as desired. But now the "defaults" overwrite any user specified values. E.g. if I change the my $hamper line to be

my $hamper = Hamper.new( items => ('Dog', 'Cat', 'Sausage') );

The output without the unless @!items is still

Name: Christmas Basket Items: Mince Pie White Wine Stinky Cheese

which is not what anyone would want. So is that error message bad advice - or otherwise what is happening? Why is it that unless @!items seems to be triggered when @!items is surely empty?

Unfortunately I'm nowhere near done yet!

Say I keep the line which assigns the "defaults", without the unless @!items - ie accepting (for the time being) it writes over user specified values

What happens if I try to assign more than 3 values to @!items?

method TWEAK(){ @!items := ('Mince Pie', 'White Wine', 'Stinky Cheese', 'Sardines' +, 'Dogfood'); }

The output is then:

Name: Christmas Basket Items: Mince Pie White Wine Stinky Cheese Sardines Dogfood

Yup.. it appears to completely ignore the 3 element limit that (I believed) was placed on @!items. Shouldn't it be falling over? What is with that? (Ok as I write this I think I can hazard a guess - the answer is going to be something along the lines of "the fixed limit restriction applies to the accessor and not to the underlying attribute, and since in TWEAK you are accessing the attribute directly the restriction is circumvented." Fine if this is the case...

But final thing. My @!items are all strings. Say I want to etch that in stone:

has Str @.items[3];

(At least, I am under the impression that Str used in that context should apply a restriction to the elements of @.items... or did I get that wrong?)

So now my code looks like this:

class Hamper { has $.name = 'Christmas Basket'; has Str @.items[3] is rw; method TWEAK(){ @!items := ('Mince Pie', 'White Wine', 'Stinky Cheese'); } } my $hamper = Hamper.new; say "Name: " ~ $hamper.name; say "Items: " ~ $hamper.items;

And drumroll for the output...

Type check failed in binding; expected Positional[Str] but got List (( +"Mince Pie", "White...) in method TWEAK at script.raku line 9 in block <unit> at script.raku line 13

So what's going on there? Did I inadvertently define a 3 character string/some other weird entity? I realise the vagueness of my understanding of what a 'Positional' vs 'List' and/or 'Array' may be the bottleneck here.

I guess this is very rambling way of simply asking, "In Raku, how do you define an object attribute that is a fixed length list with typed elements, and assign defaults to it?" However, I thought I'd post in tedious detail in the hope of rectifying all aspects of my confusion.

Thanks for reading this far. You should think about going on holiday. You deserve it.

  • Comment on [Raku] Assigning defaults to attributes that are fixed length lists (and other confusions)
  • Select or Download Code

Replies are listed 'Best First'.
Re: [Raku] Assigning defaults to attributes that are fixed length lists (and other confusions)
by duelafn (Vicar) on Apr 28, 2021 at 13:01 UTC

    If in your first attempt (with unless @!items) you instead use: say "Items: " ~ $hamper.items.perl; you will get

    Name: Christmas Basket Items: Array.new(:shape(3,), [Any, Any, Any])

    The code has @.items[3] is rw; creates a positional attribute which contains a fixed-length array with 3 elements. Thus, the array is truthy and the unless is not triggered.

    When you change to @!items := ('Mince Pie', 'White Wine', 'Stinky Cheese', 'Sardines', 'Dogfood'); you replace the fixed-kength array with a new List. Since @!items is rw that is allowed -- you aren't modifying the fixed length array, you are replacing it. Since the [3] describes the array, not the attribute, this is all fine -- to raku, though not perhaps to you :).

    There may be a way to do this using has, but at some point it is reasonable to write your own accessor:

    class Hamper { has $.name = 'Christmas Basket'; has Str @!items; method TWEAK(){ self.items = 'Mince Pie', 'White Wine', 'Stinky Cheese' unless + @!items; } method items() is rw { return-rw Proxy.new: FETCH => sub ($) { return @!items }, STORE => sub ($, @items) { die "Wrong length" unless @items.elems == 3; @!items = @items; }; } } my $hamper = Hamper.new; # $hamper.items = 'Mince Pie', 'White Wine', 'Stinky Cheese', 'Dogfood +'; say "Name: " ~ $hamper.name; say "Items: " ~ $hamper.items;

    Good Day,
        Dean

      Hi Dean - thanks for that, you really cleared a lot up there.

      "The code has @.items[3] is rw; creates a positional attribute which contains a fixed-length array with 3 elements. Thus, the array is truthy and the unless is not triggered."

      I guess this is just a matter of convention and I am happy for fixed (non-zero) length arrays to be regarded as "true". That would make me right about the error message being slightly misleading, I think?

      Thanks for steering me in the direction of writing my own accessor. I am aware that this is an option - though I haven't experimented with it yet. Actually I was hoping it could be done with has, as I am expecting (too much of?) Raku code to be pretty and boilerplate free. The solution you suggested is a great help - but does seem like quite a lot of code lines. I guess this is just because it's a workaround until the ability to assigning defaults to fixed length arrays gets implemented. Then it will be reduced back to a single line (I suppose).

      Thanks again for your help :)

        Posting a quick update to this as my new and improved understanding of attributes led me to a simple alternative which gave me what I wanted. I realised my main problem was the unless having the wrong effect. The phenomenon of overwriting the fixed length array with a longer one is really just a curiosity, since I don't think there's any practical situation when you'd define an N sized array, and then specify an N + 1 element array as the default. I think the unless issue can be resolved easily in either of 2 ways, depending on what effect is desired:

        class Hamper { has $.name = 'Christmas Basket'; my Str @item_defaults[3] = 'Mince Pie', 'White Wine', 'Stinky Chee +se'; has Str @.items[3] is rw; method TWEAK(){ @!items[ $_ ] ||= @item_defaults[ $_ ] for @item_defaults.keys +; } } my $hamper = Hamper.new; say "Name: " ~ $hamper.name; say "Items: " ~ $hamper.items;

        This simply has code in TWEAK assign defaults on a per element basis. This way you get defaults at the element level, so e.g. if you do this:

        my $hamper = Hamper.new( items => ('Fish', 'Canoe') );

        $hamper.items will end up with the user specified values Fish and Canoe as the first 2 elements, and Stinky Cheese as the 3rd. This might be good for some purposes, but in my case I wanted the defaults to be completely overwritten if the user specified any values for @!items at all. ie in the above example, asking for $hamper.items should return "uninitialized value" for the third item. Only if I don't try to set any items at all should I get the defaults. This seems quite straightforward to implement with grep:

        method TWEAK(){ @!items = @item_defaults unless grep {$_}, @!items; }

        This now achieves everything I hoped:

        • I can insert up to 3 values
        • I get an error trying to insert more
        • I get an error trying to insert a non-Str value
        • I get a set of defaults if I don't specify any at all

        I think if you want exactly 3 (or however many) elements, then writing your own accessor is very likely your best option. (Or perhaps subclassing Array? Not really sure of the details there)

        Anyway, I just thought I'd share in case it helps anyone else out...

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: perlquestion [id://11131766]
Approved by erzuuli
Front-paged by erzuuli
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others chilling in the Monastery: (6)
As of 2021-05-11 05:35 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?
    Perl 7 will be out ...





    Results (113 votes). Check out past polls.

    Notices?