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?

tjwrona1992
  • 8,614
  • 8
  • 35
  • 98
  • 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. – tjwrona1992 Feb 03 '16 at 17:55
  • There's [MooseX::LvalueAttribute](https://metacpan.org/pod/MooseX::LvalueAttribute) if you're using Moose. – ThisSuitIsBlackNot Feb 03 '16 at 18:05
  • 2
    Despite the fancy idea and the solutions below, I have to say that I agree with [ysth](http://stackoverflow.com/a/35184272/622310) 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 understands – Borodin Feb 03 '16 at 18:40

3 Answers3

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.

Borodin
  • 126,100
  • 9
  • 70
  • 144
ikegami
  • 367,544
  • 15
  • 269
  • 518
  • 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! – tjwrona1992 Feb 03 '16 at 19:01
  • @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. – tjwrona1992 Feb 03 '16 at 19:25
  • @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 work – Borodin Feb 03 '16 at 22:04
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.

LeoNerd
  • 8,344
  • 1
  • 29
  • 36
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.

Borodin
  • 126,100
  • 9
  • 70
  • 144
ysth
  • 96,171
  • 6
  • 121
  • 214
  • 1
    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. – ikegami Feb 03 '16 at 18:50
  • 1
    @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. – ysth Feb 03 '16 at 19:04
  • 1
    That's the same as `want('LVALUE')`. – ikegami Feb 03 '16 at 19:25
  • @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() = 1` – Borodin Feb 03 '16 at 21:57