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

Are there ways in perl6 to write more complex attribute validators and/or before/after/around modifiers on attribute access? I can't seem to find anything that would let me, say, implement an interval class with a lower and upper bound, or call some code every time an attribute is changed. Did I miss it somewhere?

class Interval { has Real $.lb is rw = die 'Lower bound is required'; has Real $.ub is rw = die 'Upper bound is required'; } ### Example which does not work: # class Interval { # has Real $.lb where * <= $.ub is rw = die 'Lower bound is requir +ed'; # has Real $.ub where * >= $.lb is rw = die 'Upper bound is requir +ed'; # after lb { say "Lower bound was changed!" if @_ } # } my $i = Interval.new( lb => 1, ub => 3); say $i.perl; $i.lb = 0; # should print message say $i.perl; $i.lb = 6; # should blow up! say $i.perl;

Update: Thanks all, tye makes a great point about the folly of such a design and raiph hunted down some additional options — though implemented as lvalue methods, we can not distinguish reading from writing.

To me, it looks like the is rw trait on attributes should, in most cases, be avoided. Using it does limit what you are able to validate (fundamentally, not due to language shortcomings). Thus a recommended alternate implementation:

class Interval { has Real $.lb = die 'Lower bound is required'; has Real $.ub = die 'Upper bound is required'; method update(:$lb = $.lb, :$ub = $.ub) { die "Require lb <= ub" unless $lb <= $ub; ($!lb, $!ub) = ($lb, $ub); } }

And if one really wants independent validating setters or distinct read/write triggers, we have to implement both ourselves and must use method call notation (no lvalue $i.lb = 6 syntax). Update: Apparently there is, in fact, a way to save the lvalue syntax, though the linked lvalue approach doesn't seem to be working currently. Here is the method-based approach:

class Interval { has Real $.lb = die 'Lower bound is required'; has Real $.ub = die 'Upper bound is required'; multi method lb() { $!lb } multi method lb($lb) { die "Require lb <= ub" unless $lb <= $!ub; $!lb = $lb; } multi method ub() { $!ub } multi method ub($ub) { die "Require lb <= ub" unless $!lb <= $ub; $!ub = $ub; } }

Good Day,
    Dean

Replies are listed 'Best First'.
Re: [perl6] Complex Attribute Validation and/or Triggers (documentation & tuple)
by LanX (Saint) on Mar 07, 2015 at 14:35 UTC
    A general suggestion:

    A link to the documentation (here attribute validation) would be helpful for those who never used Perl 6. :)

    update

    From my understanding do the bounds belong to a 2-tuple, but you are handling them individually.

    I'd try to see if Perl 6 easily allows to handle them in a combined data structure. Validation should be easy then.

    Cheers Rolf
    (addicted to the Perl Programming Language and ☆☆☆☆ :)

    PS: Je suis Charlie!

Re: [perl6] Complex Attribute Validation and/or Triggers
by raiph (Deacon) on Mar 09, 2015 at 03:56 UTC
    Updated July 2015 for coherence and to note related SO qa.

    Hi Dean,

    First, thanks for exploring Perl 6. I hope it's fun. :)

    If you use the `is rw` trait on an attribute, you're asking the compiler to automatically generate a setter for you, a method with the same name as the attribute. This method will be a lvalue routine. And the setter method syntax (eg `$i.lb = 6`) will work.

    You create a custom setter method by explicitly declaring a method with the same name as the attribute. You should mark it `is rw` and return a Proxy object with suitable FETCH (read) and STORE (write) routines.

    However, please know that, in addition to eluding documentation, Proxies may lead you to encounter bugs, not yet implemented stuff, missing sugar, etc.

    (In the Perl 6 world, unlike in the Perl 5 world, the current implementation and especially documentation state of things is often LTA. Both the software and doc (activity) continue to improve but they've still got a long, long way to go.)

      Indeed it is fun. I love the language and now with Inline::Perl5 the number of projects I'm willing to try it out on is greatly increased!

      Thanks for all your research and link to proxys. Indeed, it looks like these can save us if we were to create a class with a publicly writable attribute which really should have been implemented as a method. And, indeed there do seem to be bugs lurking still. I can't quite get it to work now, but am sufficiently satisfied that something like this will eventually:

      class Interval { has Real $.lb = die 'Lower bound is required'; has Real $.ub = die 'Upper bound is required'; method lb() is rw { return Proxy.new: FETCH => method () { return $!lb; }, STORE => method ($lb) { die "Require lb <= ub" unless $lb <= $!ub; $!lb = $lb; }; } method ub() is rw { return Proxy.new: FETCH => method () { return $!ub; }, STORE => method ($ub) { die "Require lb <= ub" unless $!lb <= $ub; $!ub = $ub; }; } }

      Good Day,
          Dean

        Updated with info about bug. Moved Interop section to its own post.

        Glad to hear you're having fun. :)

        if we were to create a class with a publicly writable attribute

        All attributes are private. No matter what. All public access is via public methods. Another comment covers this in more detail.

        I can't quite get it to work now:

        After some digging I've concluded that one should not use `method` as the routine declarator for the value of the FETCH and STORE arguments to the Proxy but rather `sub`:

        # This is NOT a good way to validate for this use case. # But this code works in my current Rakudo. class Interval { has $.lb = die 'Lower bound is required'; has $.ub = die 'Upper bound is required'; method lb() is rw { return Proxy.new: # note `sub` declarator and ignored arg: FETCH => sub ($) { return $!lb; }, # note `sub` declarator and ignored (first) arg STORE => sub ($, $lb) { die "Require lb <= ub" unless $lb <= $!ub; $!lb = $lb; }; } method ub() is rw { return Proxy.new: FETCH => sub ($) { return $!ub; }, STORE => sub ($, $ub) { die "Require lb <= ub" unless $!lb <= $ub; $!ub = $ub; }; } }

        Using the `method` declarator as a FETCH or STORE routine isn't useful because the self in the called routine (passed in as the first arg, corresponding to the anonymous `$` parameter in the above declarations) would not be the enclosing class.

        Update Has been discussed on #perl6. jnthn landed a fix for a couple of related bugs a few hours later. Your code still won't work though; fixing it will apparently have to wait for now.

Re: [perl6] Complex Attribute Validation and/or Triggers (good OO)
by tye (Sage) on Mar 06, 2015 at 18:43 UTC

    Gah!

    Just write a freakin' method!

    - tye        

      I can accept that. However, if there truly is no way of doing it with attributes, it makes me a bit sad since it makes using perl6 public attributes "dangerous". If I start a simple project using attributes and later determine that I need some additional checks or triggers that are not possible using attributes then I have to go back and munge a bunch syntax, replacing $i.lb = 6 with $i.lb(6). This would turn attributes into a "nice feature that I can't use without feeling guilty".

      Good Day,
          Dean

        if there truly is no way of doing it with attributes, it makes me a bit sad

        My comment had nothing to do with whether or not anything is possible in Perl 6.

        it makes using perl6 public attributes "dangerous"

        Duh. Yeah, public attributes are a bad idea.

        Even if you manage to make $i.lb = 6; fatal when 4 == $i.ub, then users of your object are likely to do:

        $i.lb = 6; $i.ub = 7;

        and have it die half-way through.

        OO in the style of "a bag of attributes with wrappers around setters (types, 'before's, etc.)" is just a terrible (anti-modular) approach to OO (despite it being extremely common).

        - tye        

        »»» This post is about alpha status Perl 6, not rock solid Perl 5 «««

        tl;dr A) You're already using automatically generated method calls without knowing it. B) Your assignments will continue to work if you override these with your own manually written attribute accessors. C) There's more to this than meets the eye. ;)

        If one compiles this:

        class c { has $.lb }

        the compiler creates both a private read-only attribute and a public read-only accessor method. It's as if one had written:

        class c { has $!lb; #| Auto-generated if there's no explicit `lb` method: method lb { $!lb } }

        The generated `lb` accessor method provides public read-only access to the otherwise private read-only `$!lb` attribute.

        If one writes:

        class c { has $.lb is rw; } # note the `is rw`

        the compiler automatically creates a corresponding lvalue method that provides read/write access as if you had written:

        class c { has $!lb; #| Auto-generated if there's no explicit `lb` method: method lb is rw { $!lb } # note the `is rw` }

        When the compiler sees code of the form `$i.lb = 6` it calls the `lb` method and treats the result as a container to which the RHS (the `6`) is assigned. For example one could write the following bizarre but hopefully self-explanatory code:

        my $container; class c { #| `is rw` means return the container, not its value method m is rw { $container } }; my $o = c.new; $o.m = 6; say $container # prints `6`, the container's contents


        With that confusion out of the way (hopefully I haven't created more!) you may be interested in the following (edited) dialog about your Interval example use case:

        raiph    What's the intended way to create
                 (the equivalent of) validating setter methods?
        jnthn    Preferably, fold the validation into the type
                 (using subset types)
        raiph    will a subset type on an attribute support access to self?
        jnthn    No
                 If you want an l-value-ish interface where
                 you can do what you like, there's Proxy
                 But an object with interesting enough invariants
                 that you want to do more sophisticated things
                 probably deserves an interface expressed in
                 terms of meaningful method names rather
                 than just getter/setter logic.
        raiph    fwiw the use case is an Interval class with
                 lower/upper bounds; setters for those; and
                 the invariant lower <= upper.
        jnthn    I lack context, but off-hand, Interval sounds
                 to me like a value object, and so the kinda thing
                 where I'd go for an immutable design.
                 (And validate at construction time)
        


        Which brings me to my cryptic "more to this than meets the eye" comment at the start. There's tye's and jnthn's admonitions. And more than I was expecting of: lack of sugar; bugs; not yet implemented stuff. YMMV but, at least in the near term (this year? next?) may not...

        It may help to think of constraints on attributes as being either static or dynamic (I'm just making up these terms). Those that only take into account the attribute itself and don't rely on external values are static; numbers being non-negative, strings being non-empty, that sort of thing.

        By contrast, requiring that the lower bound of an interval does not exceed its upper bound is a dynamic constraint: you can't know whether the constraint is violated without actually checking the upper bound, i.e. accessing the interval object.

        It may thus help to use public attributes only when you can be sure that all constraints are static in nature. If you need dynamic constraints, use methods so you have the language's full power at your disposal for verification.

Re: [perl6] Complex Attribute Validation and/or Triggers
by trwww (Priest) on Mar 08, 2015 at 02:51 UTC
      Yes, we have it. This became the Proxy feature described under Lvalue routines in the design docs. They are already implemented in Rakudo, though I'll note there are some wrinkles as I mentioned in another comment I just posted to this thread. I hope to return to elaborate on Proxies in this thread in coming weeks/months.