Check if a subroutine is being used as an lvalue or an rvalue in Perl
Asked Answered
O

3

9

I'm writing some code where I am using a subroutine as both an lvalue and an rvalue to read and write database values. The problem is, I want it to react differently based on whether it is being used as an lvalue or an rvalue.

I want the subroutine to write to the database when it is used as an lvalue, and read from the database when it is used as an rvalue.

Example:

# Write some data
$database->record_name($subscript) = $value;

# Read some data
my $value = $database->record_name($subscript);

The only way I can think of the make this work is to find a way for the subroutine to recognize whether it is being used as an lvalue or an rvalue and react differently for each case.

Is there a way to do this?

Olav answered 3/2, 2016 at 17:40 Comment(3)
I agree, but I'm trying to mirror the functionality of a similar API used in Python for database access and it just seems to look cleaner from a UI perspective using an lvalue subroutine.Olav
There's MooseX::LvalueAttribute if you're using Moose.Lorrielorrimer
Despite the fancy idea and the solutions below, I have to say that I agree with ysth who suggests that you should just call $database->record_name($subscript, $value) to write to the database. Then all your subroutine has to do is check for the existence of the second parameter. Anything more elaborate than that and you will be creating code that no one else understandsBaluchistan
A
5

Deciding how to behave on whether it was called as an lvalue or not is a bad idea since foo(record_name(...)) would call it as an lvalue.

Instead, you should decide how to behave on whether it is used as an lvalue or not.

You can do that by returning a magical value.

use Variable::Magic qw( cast wizard );

my $wiz = wizard(
   data => sub { shift; \@_ },
   get => sub { my ($ref, $args) = @_; $$ref = get_record_name(@$args); },
   set => sub { my ($ref, $args) = @_; set_record_name(@$args, $$ref); },
);

sub record_name :lvalue {
   cast(my $rv, $wiz, @_);
   return $rv;
}

A little test:

use Data::Dumper;

sub get_record_name { print("get: @_\n"); return "val"; }
sub set_record_name { print("set: @_\n"); }

my $x = record_name("abc", "def");        # Called as rvalue

record_name("abc", "def") = "xyz";        # Called as lvalue. Used as lvalue.

my $y_ref = \record_name("abc", "def");   # Called as lvalue.
my $y = $$y_ref;                          #   Used as rvalue.
$$y_ref = "xyz";                          #   Used as lvalue.

Output:

get: abc def
set: abc def xyz
get: abc def
set: abc def xyz

After seeing this, you've surely learned that you should abandon the idea of using an lvalue sub. It's possible to hide all that complexity (such as by using sentinel), but the complexity remains. The fanciness is not worth all the complexity. Use separate setters and getters or use an accessor whose role is based on the number of parameters passed to it ($s=acc(); vs acc($s)) instead.

Adela answered 3/2, 2016 at 18:10 Comment(3)
This answer is awesomely confusing haha. In the end I've decided to just go with avoiding lvalue subroutines altogether, but I am really intrigued by the idea of returning magical values!Olav
@Borodin: Regardless of what I've decided to do myself, this is the only answer that truly answers the question and explains how to do what was asked. I'll admit I have not tested it out myself, nor do i fully understand the solution, but I've never known ikegami to give a bad answer so I trust this solution will work.Olav
@tjwrona1992: Another issue is that it would be very awkward to use an lvalue subroutine as a setter, because the subroutine doesn't see the rvalue at all - it just returns a variable to which the rvalue will be assigned, and unless you return an object that has assignment overloaded that isn't going to workBaluchistan
A
4

For this situation you might like to try my Sentinel module.

It provides a function you can use in the accessor, to turn it into a more get/set style approach. E.g. you could

use Sentinel qw( sentinel );

sub get_record_name { ... }
sub set_record_name { ... }

sub record_name
{
   sentinel get => \&get_record_name,
            set => \&set_record_name,
            obj => shift;
}

At this point, the following pairs of lines of code are equivalent

$name = $record->record_name;
$name = $record->get_record_name;

$record->record_name = $new_name;
$record->set_record_name( $new_name );

Of course, if you're not needing to provide the specific get_ and set_ prefixed versions of the methods as well, you could inline them as closures.

See the module docs also for further ideas.

Accurate answered 3/2, 2016 at 17:59 Comment(0)
A
0

In my opinion, lvalue subroutines in Perl were a dumb idea. Just support ->record_name($subscript, $value) as a setter and ->record_name($subscript) as a getter.

That said, you can use the Want module, like this

use Want;

sub record_name:lvalue {
    if ( want('LVALUE') ) {
        ...
    }
    else {
        ...
    }
}

though that will also treat this as an LVALUE:

foo( $database->record_name($subscript) );

If you want only assignment statements to be treated specially, use want('ASSIGN') instead.

Atul answered 3/2, 2016 at 17:57 Comment(4)
If you use want('LVALUE'), then f(record_name) fails (assuming you want to pass the record name to f). But if you use want('ASSIGN'), then $_ = 1 for recordname; breaks. This approach is inherently flawed.Adela
@ikegami: yup, it is. I've hand-coded something that worked like your answer before, but didn't know there was a module for it. In any case, the whole thing makes for a sucky interface.Atul
That's the same as want('LVALUE').Adela
@ikegami: You're right. My test ouput was confused by semi-panic: attempt to dup freed string so I interpreted the results wrongly, but that's another reason not to use Want. The error was induced by a simple proc() = 1Baluchistan

© 2022 - 2024 — McMap. All rights reserved.