Beefy Boxes and Bandwidth Generously Provided by pair Networks
Perl: the Markov chain saw

To Validate Data In Lvalue Subs

by Zaxo (Archbishop)
on Sep 03, 2003 at 00:11 UTC ( #288461=perlmeditation: print w/replies, xml ) Need Help??

One rap against the implementation of OO Perl by lvalue closures is that no hook is available for checking the validity of input. Code before the last line of an lvalue sub is ignored in lvalue assignment.

It struck me that all that is needed is some user sub that can subvert assignment. I realized that if the closure were on a tied variable, the STORE method could do that. Time for an experiment...

For this demo, I define a tied scalar package, Constrained, which takes a coderef in its constructor. The sub takes one argument and returns true if the argument is valid, false otherwise. I'll fill in the details in the listing.

After preliminaries,

#!/usr/bin/perl use warnings; use strict;
I define the Constrained namespace, which is to implement a tied scalar checked against a user-supplied validation routine before storing a value. Errors will throw POSIX EINVAL.
package Constrained; use Errno qw/EINVAL/;
The _invalidate sub is called with an Errno as argument. This technique is likely to be used with numeric constraints, where EDOM and ERANGE might be more natural. I don't make any use of that here.
sub _invalidate { $! = shift; die sprintf('Constraint violation: %s by %s::%s in %s line %s.', $!, map { qq($_) } (caller 1)[0,3,1,2] ), "\n"; }
As an exception to the constraint regime, I permit a newly tied variable to be undef if no initializer is supplied. That will make it necessary to rule that definedness is required in the FETCH method below.
sub TIESCALAR { my $class = shift; if (defined $_[1]) { $_[0]($_[1]) or _invalidate EINVAL; } bless { code => $_[0], val => $_[1]}, $class; }
STORE() is called in assignment and modification, so we highjack it to throw if fed bad data.
sub STORE { my ($self, $val) = @_; $self->{code}($val) or _invalidate EINVAL; $self->{val} = $val; }
FETCH() is called when anything reads the value. We require definedness so that we get tested data back.
sub FETCH { defined $_[0]{val} or _invalidate EINVAL; $_[0]->{val}; } sub DESTROY { # nothing to do } 1;
To try the idea, let's produce a tied scalar which can only be assigned strings of the form 'cvvcc' (English vowels and consonants). We get back into main::, cook up some shorthand for vowel and consonant character classes, and fire up the closure on the tied variable.
package main; my ($v, $c) = (qr/[aeiouy]/i, qr/[bcdfghjklmnpqrstvwxyz]/i); { tie my $foo, 'Constrained', sub {shift=~/^$c$v$v$c$c$/}; sub foo () :lvalue { $foo } }
Finally, we run a series of tests to show what's working.
print q(Testing FETCH error for undefined value,), $/; defined( eval { print 'foo is ', foo, $/ }) or print $@, $/; print q(Testing STORE error for invalid value, 'quasi',), $/; print defined( eval { foo = 'quasi'}) ? ('foo is ', foo, $/, $/) : ($@, $/); print q(Testing STORE error valid value, 'quash',), $/; print defined( eval { foo = 'quash'}) ? ('foo is ', foo, $/, $/) : ($@, $/); print q(Testing modification, 'foo =~ s/h/e/',), $/; print defined( eval { foo =~ s/h/e/}) ? ('foo is ', foo, ' - ERROR', $/, $/) : ($@, $/); print q(Testing TIESCALAR error invalid value, 'suite',), $/; print defined( eval { tie my $bar, 'Constrained', sub {shift=~/^$c$v$v$c$c$/}, 'suite'; sub bar () :lvalue { $bar } }) ? ('bar is ', bar, $/, $/) : ($@, $/); print q(Testing TIESCALAR error valid value, 'suits',), $/; print defined( eval { tie my $baz, 'Constrained', sub {shift=~/^$c$v$v$c$c$/}, 'suits'; sub baz () :lvalue { $baz } }) ? ('baz is ', baz, $/, $/) : ($@, $/);
Here are the results:
__END__ Testing FETCH error for undefined value, Constraint violation: Invalid argument by main::Constrained::FETCH in line 50. Testing STORE error for invalid value, 'quasi', Constraint violation: Invalid argument by main::Constrained::STORE in line 53. Testing STORE error valid value, 'quash', foo is quash Testing modification, 'foo =~ s/h/e/', Constraint violation: Invalid argument by main::Constrained::STORE in line 63. Testing TIESCALAR error invalid value, 'suite', Constraint violation: Invalid argument by main::Constrained::TIESCALAR + in line 70. Testing TIESCALAR error valid value, 'suits', baz is suits
... and that's that. It was very satisfying to see the s/// modification test pass. It was an afterthought which I feared would break the idea.

Destructive comments will be treasured.

After Compline,

Replies are listed 'Best First'.
Re: To Validate Data In Lvalue Subs
by blokhead (Monsignor) on Sep 03, 2003 at 05:06 UTC
    Am I being dense, or is this more or less what Juerd's Attribute::Property module does? You mention using these lvalue subs for OO but I see your example doesn't use method calls per se -- you seem to have your example lvalue subs fixed to a single tied object which seems impractical for everyday OO use. If your goal is to generalize this approach for lvalue method calls, is it different than Attribute::Property?

    I'm really not trying to be difficult: I just want to make sure I'm not missing the point. ;) In any case, Juerd's and your goals seem to have a lot in common.


      Thanks for the link, I had a look at Attribute::Property and it does indeed, deep down, also use a tied interface to validate data. The public interface is quite different, A::P being polished up to use attribute notation.

      My preferred direction for this is different. I like to keep the nuts and bolts within reach. I'd extend Constrained to a family of tied classes with different data structures and exception policies. Some sort of class factory to produce blessed coderefs with cloistered data from the body of the constructor seems likely.

      After Compline,

Re: To Validate Data In Lvalue Subs
by perrin (Chancellor) on Sep 03, 2003 at 04:49 UTC
    When you speak of "the implementation of OO Perl by lvalue closures" as something that would have a rap against it, are you referring to an idea that has a rap for it somewhere? Is there some prior art that you can refer us to here?
Re: To Validate Data In Lvalue Subs
by dragonchild (Archbishop) on Sep 03, 2003 at 12:26 UTC
    A personal nit here - I've played with tieing to provide hard encapsulation and found that it slowed down accessing the object by at least 50%. I wasn't limiting myself to just validation, but tie creates a lot of overhead. Frankly, I don't see the sugary benefits here outweighing the large performance hit. I do a lot of CGI work where SLA's have to be met and hardware simply will not be increased this year. Performance, while not the first priority, simply cannot be the last priority. I use OO for maintainability. I don't want it to also become a liability.

    We are the carpenters and bricklayers of the Information Age.

    The idea is a little like C++ templates, except not quite so brain-meltingly complicated. -- TheDamian, Exegesis 6

    Please remember that I'm crufty and crochety. All opinions are purely mine and all code is untested, unless otherwise specified.

Re: To Validate Data In Lvalue Subs
by fletcher_the_dog (Friar) on Sep 03, 2003 at 17:04 UTC
    One thing you can do instead of using a tie is make your lvalue subs also have the ability to act as getters and setters. Then when you are using the object in a context where you are in control of the data you can use the lvalue assignment, but then when you want to verify data you can you use the sub in the setter mode. Example:
    #!/usr/bin/perl use strict; package Foo; sub new{ my $class = shift; my $self = { bar=>"" }; bless $self,$class; } sub bar : lvalue { my $self = shift; if (@_) { # validate my $value = shift; if (ref($value)) { die "foo->bar must not be a reference!\n"; } $self->{bar}=$value; } $self->{bar} } package main; my $test = Foo->new; # Use lvalue assignment $test->bar = "Hello"; print $test->bar."\n"; # Use setter mode to verify data $test->bar("There"); print $test->bar."\n"; # Use setter mode to verify data $test->bar($test); print $test->bar."\n"; __OUTPUT__ Hello There foo->bar must not be a reference!
    Of course this should be well documented in your pod so people don't accidently alter the lvalue.
Re: To Validate Data In Lvalue Subs
by adrianh (Chancellor) on Sep 13, 2003 at 23:02 UTC

    You can also do this sort of thing with Tie::OneOff. For example:

    use Tie::OneOff; { my $i; sub int_only() : lvalue { +Tie::OneOff->lvalue({ STORE => sub { $i = ($_[0] =~ m/^\d+$/s) ? $_[0] : die "ba +d int" }, FETCH => sub { $i }, }) }; } use Test::More 'no_plan'; use Test::Exception; lives_ok { int_only = 42 } 'can set int_only to be an integer'; is int_only, 42, 'value set okay'; dies_ok { int_only = "a" } 'cannot set it to be a non-int'; is int_only, 42, 'old value preserved'; int_only =~ s/42/24/s; is int_only, 24, 'substitution works'; dies_ok {int_only =~ s/4/a/s } 'cannot replace number with letter';

Log In?

What's my password?
Create A New User
Node Status?
node history
Node Type: perlmeditation [id://288461]
Approved by herveus
Front-paged by Limbic~Region
[Eily]: oh, boolean context uses the "" overload if no bool overload has been defined
[Eily]: (I wondered if overloading bool was actually necessary)
[Corion]: Yeah, you need bool to get a true value, and the rest to return the other value ;)
[choroba]: perl -wE '{package o; use Tie::Scalar; use parent -norequire => "Tie::StdScalar" ;sub FETCH { "x" x int rand 2}} tie my $x, "o" ; say $x for 1 .. 10'
[choroba]: for complete ness

How do I use this? | Other CB clients
Other Users?
Others studying the Monastery: (9)
As of 2017-07-27 13:52 GMT
Find Nodes?
    Voting Booth?
    I came, I saw, I ...

    Results (414 votes). Check out past polls.