Beefy Boxes and Bandwidth Generously Provided by pair Networks
There's more than one way to do things
 
PerlMonks  

Returning a tied scalar

by MarkusLaker (Beadle)
on Apr 16, 2005 at 10:46 UTC ( [id://448441]=perlquestion: print w/replies, xml ) Need Help??

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

A colleague asked if there was a way to make a Perl program die as soon as a particular variable became undefined. There's a more general question waiting to be asked: can we construct a variable that throws an exception as soon as you assign a value that doesn't meet some user-provided condition?

It turns out that we can. Here's a proof of principle:

#!/usr/bin/perl -l use warnings; use strict; package Checkee; # A tied object that has a value and a closure. Any value that the # caller wants to assign to the object is first run through the # closure; if the result is false, we die with a backtrace. use fields qw/Value Closure/; use Carp; sub TIESCALAR { my ($class, $closure) = @_; my Checkee $self = fields::new; $$self{Closure} = $closure; bless $self; } sub FETCH { (my Checkee $self) = @_; $$self{Value}; } sub STORE { (my Checkee $self, local $_) = @_; confess "Illegal assignment: ", defined $_? $_: '[undef]' unless $$self{Closure}(); $$self{Value} = $_; } package CheckFactory; # An object that stores a closure. It can create tied objects that # use the closure to check any value assigned to them. use fields 'Closure'; sub new(&) { my ($closure) = @_; die "CheckFactory::new must be passed a closure\n" unless ref $closure eq 'CODE'; my CheckFactory $self = fields::new; $$self{Closure} = $closure; bless $self; } # Calling Syntax could be a lot prettier if we found a way to return # tied objects from a sub. sub Monitor: lvalue { (my CheckFactory $self, my $rvar) = @_; tie $$rvar, 'Checkee', $$self{Closure}; $$rvar; } package main; # Proof o' the pudding... sub deftest() { my $deffactory = CheckFactory::new {defined}; $deffactory->Monitor(\ my $var); print($var = 'Defined'); # print($var = undef); } sub inttest() { my $intfactory = CheckFactory::new {!defined or /^ -? \d+ $/x}; $intfactory->Monitor(\ my $var) = 42; # assigns 42 to $var # -- far from obvious. print($var /= 2); # print($var /= 2); } sub main() { deftest; inttest; } main;

I don't like the way you have to declare a checked variable:

$intfactory->Monitor(\ my $var) = 42;

It hides the declaration of the new variable inside a function argument, and the assignment is completely unintuitive. Even worse, if you forget the backslash, the code silently does the wrong thing. I'd prefer to be able to say:

my $var = $intfactory->Make(42);

Trouble is, the hypothetical method CheckFactory::Make can't return a tied variable: if it tries, the caller will get a copy that isn't tied, and the tied variable will be destroyed when the method exits.

So is there a way around this? Can I somehow return a tied variable from a sub?

Many thanks,

Markus

Replies are listed 'Best First'.
Re: Returning a tied scalar
by nobull (Friar) on Apr 16, 2005 at 13:09 UTC
    I don't like the way you have to declare a checked variable:
    $intfactory->Monitor(\ my $var) = 42;

    Yes that's ugly. You can at least get rid of the backslash. $_[1] paseed to the method is an alias to $var so \$_[1] is \$var.

    Note also that the value of a scalar assignment is itself an lvalue.

    sub Monitor : lvalue { my CheckFactory $self = shift; my $rvar = \shift; my $lvar = $$rvar; tie $$rvar, 'Checkee', $self->{Closure}; $$rvar = $lvar; $$rvar; } $intfactory->Monitor(my $var = 42);

    If you want more syntactic sugar then take a look at attributes.

Re: Returning a tied scalar
by salva (Canon) on Apr 16, 2005 at 11:34 UTC
    if you can live without OO, using lvalue subrutines with (\$) prototypes makes things cleaner:
    sub int_monitor (\$ ) :lvalue { my $var=shift; print "$var\n"; $$var }; int_monitor(my $foo) = 14; print "$foo\n"; (int_monitor my $bar) = 15; print "$bar\n";
    or another way:
    sub int_monitor (\$;$) { my $var=shift; print "$var\n"; $$var=shift if @_ }; int_monitor my $foo => 14; print "$foo\n";

      If you can live without OO, using lvalue subrutines with (\$) prototypes makes things cleaner:

      sub int_monitor (\$ ) :lvalue { my $var=shift; print "$var\n"; $$var };

      There is no need for a prototype which also means you don't need to forgo the OO if you don't want to.

      sub int_monitor:lvalue { my $var=\shift; print "$var\n"; $$var };
        Nobull writes:
        There is no need for a prototype which also means you don't need to forgo the OO if you don't want to.

        sub int_monitor:lvalue { my $var=\shift; print "$var\n"; $$var };

        Very neat! I guess that relies on two facts: that @_ contains aliases to the args (which I knew but had forgotten) and that shift returns the first element of the array, rather than a copy of it.

        Thanks for your suggestions,

        Markus

Re: Returning a tied scalar
by diotalevi (Canon) on Apr 16, 2005 at 17:09 UTC

    The general solution for lvalue subroutines is to use Tie::Constrained.

    my $checked; tie $checked => 'Tie::Constrained', sub { defined $_[0] }, $INIT_VALUE;

    The real answer is that you can't return variables. You can return values. The variable being assigned to already exists outside of the function and is a receptacle for the value that is to be returned by the right side of the assignment operation. So tough luck. You could write some compiler macro, I suppose, if you wanted.

Re: Returning a tied scalar
by iblech (Friar) on Apr 17, 2005 at 10:12 UTC

    And there's a Perl 6 way of doing it:

    # Perl 6 my $init_value = 42; my $checked = new Proxy: FETCH => { $init_value }, STORE => -> $new { die "Error: New value must be defined!\n" unless defined $new; $init_value = $new; };

    See S06 for more information on the Proxy object.

    --Ingo

      iblech writes:
      And there's a Perl 6 way of doing it:
      *Chuckle*

      Actually, what I had the back of my mind was Perl 6's subtypes. In my original code, each CheckFactory corresponds to a named subtype, and each Checkee corresponds to an instance of the subtype.

      Zaxo's Tie::Constrained module, to which another monk helpfully pointed me, works more like an anonymous subtype, in which you have to restate the constraint every time you want to constrain a new variable. That may mean Zaxo had a different application in mind (where each variable had different constraints, and the need to define a factory function would have just made the module harder to use); or it may mean he bumped up against the same problem I did, and couldn't find a way to write a good factory function to churn out identically-constrained variables.

      Cheers,

      Markus

        That may mean Zaxo had a different application in mind (where each variable had different constraints, and the need to define a factory function would have just made the module harder to use) . . .

        You're exactly right about that. The original idea was just a bit of implementation to demonstrate validation for lvalue closures. My initial real application was to retrofit validation and detainting to cgi form processing scripts. No commonality was assumed for the variable types. It was later that I noticed more general applications for the module.

        The idea of a factory-like method for constraining lists of variables is a good one. I'll add it to the TODO list. Here's an untested first cut at it:

        use Tie::Constrained; sub Tie::Constrained::tiethese { my $self = shift; map {tie $_, 'Tie::Constrained', $self} @_; } my $integer = Tie::Constrained->new( sub { $_[0] =~ /^[+-]?\d+$/; } ); my ($i,$j,$k,$l); $integer->tiethese($i,$j,$k,$l);
        As diotalevi said, functions return lists of values, not variables, so a method to tie a bunch of variables pretty much needs to be handed the variables and work with their aliases. You may not have noticed the constructor for unbound Tie::Constrained objects. I put that in to take the anonymity out of the subtypes and allow more readable ties with often-used constraints.

        After Compline,
        Zaxo

        Of course, you can do it by using subtypes, too :)

        my subtype HasToBeDefined of Scalar where { defined $^a }; my HasToBeDefined $checked;

        --Ingo

Log In?
Username:
Password:

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

How do I use this?Last hourOther CB clients
Other Users?
Others meditating upon the Monastery: (3)
As of 2024-04-20 03:38 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found