2

How can I hide a "tie" call from the user so calling an accessor will implicitly do it for them?

I want to do this, because I have a data structure that can be accessed by the user, but values stored in this structure can be modified without the user's knowledge.

If an attribute in the data structure changes, I want any variables referencing that attribute modified as well so the user will always be using fresh data. Since the user will always want fresh data, it's simpler and more intuitive if the user doesn't even need to know it's happening.

This is what I have so far... it doesn't seem to work though, the output is:

hello
hello

What I want is:

hello
goodbye

Code:

#!/usr/bin/perl
use warnings;
use strict;
use feature qw{ say };

{
    package File;
    use Moose;

    has '_text' => (is => 'rw', isa => 'Str', required => 1);

    sub text {
        my ($self) = @_;
        tie my $text, 'FileText', $self;
        return $text;
    }
}

{
    package FileText;
    use Tie::Scalar;

    sub TIESCALAR {
        my ($class, $obj) = @_;
        return bless \$obj, $class;
    }

    sub FETCH {
        my ($self) = @_;
        return $$self->_text();
    }

    sub STORE {
        die "READ ONLY";
    }
}

my $file = 'File'->new('_text' => 'hello');

my $text = $file->text();
say $text;

$file->_text('goodbye');
say $text;
tjwrona1992
  • 8,614
  • 8
  • 35
  • 98
  • 1
    This solution adds "action at a distance"; `$text` can be changed outside the context where it's declared. The reader can not know how `$text` can change. Worse, the reader is lead to believe `$text` is a simple string and thinks it is purely lexical. This all makes code infinitely more complicated to debug. Perhaps you should ask a question about the problem you're trying to solve using tying? – Schwern Aug 06 '15 at 17:55

2 Answers2

5

I would not recommend doing this. You're introducing "action at a distance" which leads to some very difficult to catch bugs. The user thinks they're getting a string. A lexical string can only be altered by changing it directly and obviously. It has to be altered in place or obviously passed into a function or a reference attached to something.

my $text = $file->text;
say $text;  # let's say it's 'foo'

...do some stuff...
$file->text('bar');
...do some more stuff...

# I should be able to safely assume it will still be 'foo'
say $text;

That block of code is easy to understand because all the things which could affect $text are immediately visible. This is what lexical context is all about, isolating what can change a variable.

By returning a thing which can change at any time, you've quietly broken this assumption. There's no indication to the user that assumption has been broken. When they go to print $text and get bar it is non-obvious what changed $text. Anything in the whole program could change $text. That small block of code is now infinitely more complicated.

Another way to look at it is this: scalar variables in Perl have a defined interface. Part of that interface says how they can be changed. You are breaking this interface and lying to the user. This is how overloaded/tied variables are typically abused.

Whatever problem you're trying to solve, you're solving it by adding more problems, by making the code more complex and difficult to understand. I would step back and ask what problem you're trying to solve with tying.

What I would do instead is to just return a scalar reference. This alerts the user that it can be changed out from under them at any time. No magic to cover up a very important piece of information.

#!/usr/bin/perl
use warnings;
use strict;
use feature qw{ say };

{
    package File;
    use Moose;

    has 'text_ref' => (
        is              => 'rw',
        isa             => 'Ref',
        default         => sub {
            return \("");
        }
    );

    sub BUILDARGS {
        my $class = shift;
        my %args  = @_;

        # "Cast" a scalar to a scalar ref.
        if( defined $args{text} ) {
            $args{text_ref} = \(delete $args{text});
        }

        return \%args;
    }

    sub text {
        my $self = shift;

        if( @_ ) {
            # Change the existing text object.
            ${$self->text_ref} = shift;
            return;
        }
        else {
            return $self->text_ref;
        }
    }
}

my $file = 'File'->new('text' => 'hello');

my $text = $file->text();
say $$text;

$file->text('goodbye');
say $$text;

That said, here's how you do what you want.

I would recommend against using tie. It is very slow, considerably slower than a method call, buggy and quirky. One of its quirks is that the tied nature is attached to the variable itself, not the referenced data. That means you can't return a tied variable.

Instead, I would recommend using an overloaded object to store your changing text.

{
    package ChangingText;

    # Moose wants class types to be in a .pm file.  We have to explciitly
    # tell it this is a class type.
    use Moose::Util::TypeConstraints qw(class_type);
    class_type('ChangingText');

    use overload
      '""' => sub {
          my $self = shift;
          return $$self;
      },
      fallback => 1;

    sub new {
        my $class = shift;
        my $text = shift;
        return bless \$text, $class;
    }

    sub set_text {
        my $self = shift;
        my $new_text = shift;

        $$self = $new_text;

        return;
    }
}

Overloaded objects have their own caveats, mostly due to code which expects strings writing things like if !ref $arg, but they are easier to deal with than the deep tie bugs.

To make this transparent, store the ChangingText object in the File object and then put a hand made text accessor around it to handle plain strings. The accessor makes sure to reuse the same ChangingText object.

To complete the illusion, BUILDARGS is used to change plain text initialization arguments into a ChangingText object.

{
    package File;
    use Moose;

    has 'text_obj' => (
        is              => 'rw',
        isa             => 'ChangingText',
        default         => sub {
            return ChangingText->new;
        }
    );

    sub BUILDARGS {
        my $class = shift;
        my %args  = @_;

        # "Cast" plain text into a text object
        if( defined $args{text} ) {
            $args{text_obj} = ChangingText->new(delete $args{text});
        }

        return \%args;
    }

    sub text {
        my $self = shift;

        if( @_ ) {
            # Change the existing text object.
            $self->text_obj->set_text(shift);
            return;
        }
        else {
            return $self->text_obj;
        }
    }
}

Then it works transparently.

my $file = File->new('text' => 'hello');

my $text = $file->text();
say $text;  # hello

$file->text('goodbye');
say $text;  # goodbye
Schwern
  • 153,029
  • 25
  • 195
  • 336
  • That's brilliant! Now to waste 5 hours wrapping my head around the process :) Not sure I'll even use it after being told by like 12909309 different people that it's horrible practice, but thanks for actually giving me a working solution to exactly what I asked. – tjwrona1992 Aug 06 '15 at 18:47
2

return $text just returns the value of the variable, not the variable itself. You can return a reference to it, though:

sub text {
    my ($self) = @_;
    tie my $text, 'FileText', $self;
    return \$text;
}

You then have to use $$text to dereference it:

my $file = 'File'->new('_text' => 'hello');

my $text = $file->text();
say $$text;

$file->_text('goodbye');
say $$text;
choroba
  • 231,213
  • 25
  • 204
  • 289
  • Ahh, that makes sense... hmmm but what if I don't want the returned value to need to be dereferenced? Is there a way to return an alias to the `$text` from the subroutine so it can be accessed as if it were a plain scalar? – tjwrona1992 Aug 06 '15 at 17:17
  • 1
    If you're going to return a reference, there's no need for tying. Just return the reference and change it as you like. This is far simpler, faster and transparent to the user. – Schwern Aug 06 '15 at 17:59
  • @Schwern: How can you achieve `sub STORE { die 'Read only!' }` with a plain reference? – choroba Aug 07 '15 at 17:38
  • @choroba I missed that part. Use [Scalar::Readonly](https://metacpan.org/pod/Scalar::Readonly) to turn the readonly flag on and off as needed. – Schwern Aug 07 '15 at 17:49