Скрытие вызова связи от пользователя в Perl

Как я могу скрыть вызов «связи» от пользователя, чтобы вызов средства доступа неявно делал это за них?

Я хочу сделать это, потому что у меня есть структура данных, к которой пользователь может получить доступ, но значения, хранящиеся в этой структуре, могут быть изменены без ведома пользователя.

Если атрибут в структуре данных изменяется, я хочу, чтобы все переменные, ссылающиеся на этот атрибут, также изменялись, поэтому пользователь всегда будет использовать свежие данные. Поскольку пользователю всегда нужны свежие данные, проще и понятнее, если пользователю даже не нужно знать, что это происходит.

Это то, что у меня есть до сих пор... похоже, это не работает, вывод:

hello
hello

Я хочу:

hello
goodbye

Код:

#!/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;

person tjwrona1992    schedule 06.08.2015    source источник
comment
Это решение добавляет действие на расстоянии; $text можно изменить вне контекста, в котором оно объявлено. Читатель не может знать, как $text может измениться. Хуже того, читатель поверит, что $text — это простая строка, и решит, что она чисто лексическая. Все это бесконечно усложняет отладку кода. Возможно, вам следует задать вопрос о проблеме, которую вы пытаетесь решить с помощью связывания?   -  person Schwern    schedule 06.08.2015


Ответы (2)


Я бы не рекомендовал этого делать. Вы вводите «действие на расстоянии», что приводит к очень трудным для обнаружения ошибкам. Пользователь думает, что он получает строку. Лексическую строку можно изменить, только изменив ее прямо и явно. Это должно быть изменено на месте или явно передано в функцию или ссылку, прикрепленную к чему-то.

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;

Этот блок кода легко понять, потому что все, что может повлиять на $text, сразу видно. Это и есть лексический контекст, изолирующий то, что может изменить переменную.

Возвращая вещь, которая может измениться в любой момент, вы незаметно нарушили это предположение. Для пользователя нет никаких указаний на то, что предположение было нарушено. Когда они отправляются печатать $text и получают bar, неясно, что изменилось $text. Что угодно во всей программе могло измениться $text. Этот небольшой блок кода теперь бесконечно сложнее.

Другой способ взглянуть на это таков: скалярные переменные в Perl имеют определенный интерфейс. Часть этого интерфейса говорит, как их можно изменить. Вы ломаете этот интерфейс и лжете пользователю. Вот как обычно злоупотребляют перегруженными/связанными переменными.

Какую бы проблему вы ни пытались решить, вы решаете ее, добавляя новые проблемы, делая код более сложным и трудным для понимания. Я бы отступил назад и спросил, какую проблему вы пытаетесь решить с помощью связывания.

Вместо этого я бы просто вернул скалярную ссылку. Это предупреждает пользователя о том, что его можно изменить из-под них в любое время. Никакой магии, чтобы скрыть очень важную информацию.

#!/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;

Тем не менее, вот как вы делаете то, что хотите.

Я бы не рекомендовал использовать галстук. Он очень медленный, значительно медленнее, чем вызов метода, глючный и причудливый. Одна из его особенностей заключается в том, что связанный характер привязывается к самой переменной, а не к данным, на которые ссылаются. Это означает, что вы не можете вернуть связанную переменную.

Вместо этого я бы рекомендовал использовать перегруженный объект для хранения вашего изменяющегося текста.

{
    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;
    }
}

У перегруженных объектов есть свои предостережения, в основном из-за кода, который ожидает, что строки будут писать что-то вроде if !ref $arg, но с ними легче справиться, чем с ошибками глубокой связи.

Чтобы сделать это прозрачным, сохраните объект ChangingText в объекте File, а затем поместите вокруг него созданный вручную метод доступа text для обработки простых строк. Аксессор обязательно повторно использует один и тот же объект ChangingText.

Чтобы завершить иллюзию, BUILDARGS используется для изменения аргументов инициализации обычного текста в объект ChangingText.

{
    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;
        }
    }
}

Тогда он работает прозрачно.

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

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

$file->text('goodbye');
say $text;  # goodbye
person Schwern    schedule 06.08.2015
comment
Это блестяще! Теперь, чтобы потратить 5 часов на то, чтобы обернуть голову вокруг процесса :) Не уверен, что я даже буду использовать его после того, как 12909309 разных людей сказали мне, что это ужасная практика, но спасибо за то, что на самом деле дали мне рабочее решение именно то, что я просил. - person tjwrona1992; 06.08.2015

return $text просто возвращает значение переменной, а не саму переменную. Однако вы можете вернуть ссылку на него:

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

Затем вы должны использовать $$text для разыменования:

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

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

$file->_text('goodbye');
say $$text;
person choroba    schedule 06.08.2015
comment
Ах, в этом есть смысл... хммм, но что, если я не хочу, чтобы возвращаемое значение разыменовывалось? Есть ли способ вернуть псевдоним $text из подпрограммы, чтобы к нему можно было получить доступ, как если бы это был простой скаляр? - person tjwrona1992; 06.08.2015
comment
Если вы собираетесь вернуть ссылку, нет необходимости привязывать. Просто верните ссылку и измените ее по своему усмотрению. Это гораздо проще, быстрее и прозрачнее для пользователя. - person Schwern; 06.08.2015
comment
@Schwern: Как вы можете добиться sub STORE { die 'Read only!' } с помощью простой ссылки? - person choroba; 07.08.2015
comment
@choroba Я пропустил эту часть. Используйте Scalar::Readonly, чтобы включать и выключать флаг только для чтения по мере необходимости. - person Schwern; 07.08.2015