Чего следует избегать

Perl 5 не идеален. Некоторые возможности сложно использовать корректно. Другие никогда не работали хорошо. Некоторые — странные комбинации других возможностей со странными граничными случаями. Хотя лучше избегать этих возможностей, знание того, почему их нужно избегать, поможет вам найти более подходящие решения.

Голые слова

Perl — податливый язык. Вы можете писать программы в предпочитаемой вами наиболее творческой, поддерживаемой, запутанной или эксцентричной манере. Хорошие программисты заботятся о поддерживаемости, но Perl не пытается диктовать, что вы считаете поддерживаемым.

Парсер Perl понимает встроенные функции и операторы Perl. Он использует сигилы для идентификации переменных и другую пунктуацию для распознавания вызовов функций и методов. Однако, иногда парсеру приходится догадываться, что вы имеете ввиду, особенно когда вы используете голое слово — идентификатор без сигила или другой синтаксически значимой пунктуации.

Хорошее использование голых слов

Хотя прагма strict (Прагмы) справедливо запрещает неоднозначные голые слова, некоторые голые слова приемлемы.

Голые слова как ключи хеша

Ключи хеша в Perl 5 обычно не допускают неоднозначности, потому что парсер может идентифицировать их как строковые ключи; pinball в $games{pinball} — это очевидно строка.

Иногда эта интерпретация — не то, чего вы хотите, особенно если вы намеревались вычислить встроенную или пользовательскую функцию, чтобы сгенерировать ключ хеша. В этом случае, устраните неоднозначность, указав аргументы, используя круглые скобки для аргументов функции, или предварите унарным плюсом, чтобы форсировать вычисление встроенной функции:

    # ключом будет литеральный «shift»
    my $value = $items{shift};

    # ключом будет значение, возвращённое «shift»
    my $value = $items{shift @_}

    # унарный плюс приводит к использованию встроенной функции «shift»
    my $value = $items{+shift};

Голые слова как имена пакетов

Имена пакетов в Perl 5 тоже являютя голыми словами. Если вы следуете соглашениям об именовании, согласно которым имена пакетов начинаются с заглавных букв, а функций — нет, маловероятно, что вы встретитесь с коллизиями имён, но парсер Perl 5 должен определить, как парсить Package->method(). Значит ли это «вызвать функцию с именем Package() и вызвать метод method() на возвращаемом ей значении» или «вызвать метод с именем method() в пространстве имён Package»? Ответ различается в зависимости от того, какой код парсер уже встретил в текущем пространстве имён.

Принудите парсер воспринимать Package как имя пакета, добавив разделитель пакетов (::) (footnote: Даже среди тех, кто понимает, почему это работает, очень немногие это делают.):

    # вероятно метод класса
    Package->method();

    # определённо метод класса
    Package::->method();

Голые слова как имена блоков кода

Специальные именованные блоки кода AUTOLOAD, BEGIN, CHECK, DESTROY, END, INIT и UNITCHECK — это голые слова, которые объявляют функции без использования встроенной директивы sub. Вы уже видели это раньше (Кодогенерация):

    package Monkey::Butler;

    BEGIN { initialize_simians( __PACKAGE__ ) }

    sub AUTOLOAD { ... }

Хотя вы можете опустить sub из объявления AUTOLOAD(), немногие так делают.

Голые слова как константы

Константы, объявленные с помощью прагмы constant, можно использовать как голые слова:

    # не используйте это для реальной аутентификации
    use constant NAME     => 'Bucky';
    use constant PASSWORD => '|38fish!head74|';

    return unless $name eq NAME && $pass eq PASSWORD;

Обратите внимание, что эти константы не интерполируются в строках, заключённых в двойные кавычки.

Константы — специальный случай прототипированных функций (Прототипы). Когда вы предварительно объявляете функцию с прототипом, парсер знает, как воспринимать эту функцию, и будет предупреждать об ошибках неоднозначности парсинга. Все остальные недостатки прототипов всё ещё имеют место.

Неблагоразумное использование голых слов

Независимо от того, насколько внимательно вы пишете код, голые слова всё равно приводят к неоднозначностям. Вы можете избежать большинства их использований, но вам встретится несколько типов голых слов в унаследованном коде.

Голые вызовы функций

Код, написанный без strict 'subs', может использовать голые имена функций. Добавление скобок заставляет код пройти эти ограничения. Используйте perl -MO=Deparse,-p (см. perldoc B::Deparse) чтобы понять, как Perl парсит их, затем расставляйте скобки соответствующим образом.

Голые значения хешей

Некоторый старый код может не заботиться о том, чтобы заключить в кавычки значения хеш-пар:

    # плохой стиль; не использовать
    my %parents =
    (
        mother => Annette,
        father => Floyd,
    );

Если не существует ни функции Floyd(), ни Annette(), Perl будет интерпретировать эти голые слова как строки. strict 'subs' выдаст ошибку в этой ситуации.

Голые дескрипторы файлов

До появления лексических дескрипторов файлов (Ссылки на дескрипторы файлов), все дескрипторы файлов и директорий использовали голые слова. Вы почти всегда можете безопасно переписать этот код на использование лексических дескрипторов файлов; исключения — STDIN, STDOUT и STDERR. К счастью, парсер Perl распознаёт их.

Голые функции sort

Наконец, встроенная фукнция sort может принимать в качестве второго аргумента имя функции, которую нужно использовать для сортировки. Хотя это редко бывает неоднозначным для парсера, это может смутить читающих людей. Альтернатива в виде передачи ссылки на функцию в скаляре немного лучше:

    # стиль с использованием голого слова
    my @sorted = sort compare_lengths @unsorted;

    # ссылка на фукнцию в скаляре
    my $comparison = \&compare_lengths;
    my @sorted     = sort $comparison @unsorted;

Второй вариант избегает использования голого слова, но результат на одну строчку длиннее. К сожалению, парсер Perl 5 не понимает однострочную версию вследствие специального парсинга sort; вы не можете использовать произвольное выражение (такое как взятие ссылки на именованную функцию) там, где может пройти блок или скаляр.

    # не работает
    my @sorted = sort \&compare_lengths @unsorted;

В обоих случаях то, как sort вызывает функцию и передаёт аргументы, может быть запутывающим (см. perldoc -f sort для подробностей). Где возможно, рассмотрите использование вместо этого блочной формы sort. Если же вы должны использовать какую-либо из форм с функцией, рассмотрите добавление объясняющего комментария.

Непрямые объекты

В Perl 5 нет оператора new; конструктором в Perl 5 является всё, что возвращает объект. По соглашению, конструкторы — методы классов, называемые new(), но вы можете выбрать всё, что захотите. Некоторые старые руководства по объектам Perl 5 поощряют использование вызовов конструкторов в стиле C++ и Java:

    my $q = new CGI; # НЕ ИСПОЛЬЗОВАТЬ

…вместо очевидного вызова метода:

    my $q = CGI->new();

Эти варианты синтаксиса дают одинаковое поведение, за исключением случаев, когда это не так.

Непрямые вызовы с голыми словами

В непрямой объектной форме (более точно, в дательном случае) первого примера глагол (метод) предшествует существительному, к которому относится (объект). Это нормально в разговорных языках, но в Perl 5 создаёт неоднозначности парсинга.

Так как имя метода — голое слово (Голые слова), парсер должен предсказать правильную интерпретацию кода с помощью использования нескольких эвристик. Хотя эти эвристики хорошо протестированы и почти всегда корректны, их режимы сбоя сбивают с толку. Хуже того, они зависят от порядка компиляции кода и модулей.

Трудность парсинга увеличивается, если конструктор принимает аргументы. Непрямой стиль может выглядеть так:

    # НЕ ИСПОЛЬЗОВАТЬ
    my $obj = new Class( arg => $value );

…таким образом заставляя имя класса выглядеть как вызов функции. Perl 5 может устранить неоднозначность во многих подобных случаях, но его эвристики зависят от того, какие имена пакетов видит парсер, какие голые слова он уже разрешил (и как он разрешил их) и от имён функций, уже объявленных в текущем пакете.

Представьте противоречие в случае прототипированной функции (Прототипы) с именем, которому случилось каким-то образом вступить в конфликт с именем класса или метода, вызываемого непрямо. Это случается редко, но так неприятно для отладки, что стоит того, чтобы избегать непрямых вызовов.

Скалярные ограничения непрямой нотации

Другая опасность такого синтаксиса в том, что парсер ожидает объект как одно скалярное выражение. Вывод в дескриптор файла, сохранённый в агрегатной переменной, выглядит очевидным, но таковым не является:

    # НЕ РАБОТАЕТ КАК НАПИСАНО
    say $config->{output} 'Fun diagnostic message!';

Perl попытается вызвать say на объекте $config.

print, close и say — все встроенные функции, оперирующие с дескрипторами файлов — оперируют в непрямой манере. Это было нормально, когда дескрипторы файлов были глобальными переменными пакета, но лексические дескрипторы файлов (Ссылки на дескрипторы файлов) делают очевидными проблемы непрямого объектного синтаксиса. Чтобы устранить это, избавьтесь от неоднозначности подвыражения, выдающего подразумеваемый инвокант:

    say {$config->{output}} 'Fun diagnostic message!';

Альтернативы непрямой нотации

Нотация прямого вызова не страдает этой проблемой неоднозначности. Чтобы сконструировать объект, вызовите метод-конструктор напрямую на имени класса:

    my $q   = CGI->new();
    my $obj = Class->new( arg => $value );

Этот синтаксис всё ещё имеет проблему голого слова, в том смысле, что если у вас есть функция с именем CGI, Perl будет интерпретировать голое имя класса как вызов функции:

    sub CGI;

    # вы написали CGI->new(), но Perl увидел
    my $q = CGI()->new();

Хотя это случается редко, вы можете устранить неоднозначность имён классов, добавив разделитель пакетов (::) или явно пометив имена классов как строковые литералы:

    # разделитель пакетов
    my $q = CGI::->new();

    # не имеющий неоднозначности строковый литерал
    my $q = 'CGI'->new();

Однако почти никто этого не делает.

Для ограниченного случая операций с дескрипторами файлов дательное использование настолько общепринято, что вы можете использовать подход непрямого вызова, если окружите свой подразумеваемый инвокант фигурными скобками. Если вы используете Perl 5.14 (или если вы загрузили IO::File или IO::Handle), вы можете использовать методы на лексических дескрипторах файлов (footnote: Хотя почти никто не делает этого для print и say.).

CPAN-модуль Perl::Critic::Policy::Dynamic::NoIndirect (плагин для Perl::Critic) может обнаружить непрямые вызовы во время проверки кода. CPAN-модуль indirect может обнаружить и запретить их использование в запущенных программах:

    # выдать предупреждение при использовании непрямых вызовов
    no indirect;

    # выбросить исключение при их использовании
    no indirect ':fatal';

Прототипы

Прототип — это набор необязательных метаданных, присоединённых к функции, которые влияют на то, как парсер понимает её аргументы. Хотя внешне они могут выглядеть как сигнатуры функций в других языках, на самом деле они очень отличаются.

Прототипы позволяют пользователям определять свои собственные функции, ведущие себя как встроенные. Рассмотрим встроенную фукнцию push, которая принимает массив и список. Хотя обычно Perl 5 разгладил бы массив и список в единый список, переданный в push, парсер знает, что не нужно разглаживать массив, чтобы push мог модифицировать его на месте.

Прототипы функций являются частью объявления:

    sub foo        (&@);
    sub bar        ($$) { ... }
    my  $baz = sub (&&) { ... };

Любой прототип, привязанный к предварительному объявлению, должен совпадать с прототипом, привязанным к объявлению функции. Perl выдаст предупреждение, если это не так. Как ни странно, вы можете опустить прототип в предварительном объявлении и включить его в полное объявление — но делать так нет причин.

Встроенная функция prototype принимает имя функции и возвращает строку, представляющую её прототип. Используйте форму CORE::, чтобы увидеть прототип встроенной функции:

    $ perl -E "say prototype 'CORE::push';"
    \@@
    $ perl -E "say prototype 'CORE::keys';"
    \%
    $ perl -E "say prototype 'CORE::open';"
    *;$@

prototype вернёт undef для тех встроенных функций, которые вы не можете эмулировать:

    say prototype 'CORE::system' // 'undef'
    # undef; невозможно эмулировать встроенную функцию system

    say prototype 'CORE::prototype' // 'undef'
    # undef; встроенная функция prototype не имеет прототипа

Помните push?

    $ perl -E "say prototype 'CORE::push';"
    \@@

Символ @ представляет список. Обратный слеш принуждает к использованию ссылки для соответствующего аргумента. Этот прототип обозначает, что push принимает ссылку на массив и список значений. Вы можете написать mypush так:

    sub mypush (\@@)
    {
        my ($array, @rest) = @_;
        push @$array, @rest;
    }

Другие символы прототипов включают $ для скалярного аргумента, % для обозначения хеша (чаще всего используется как ссылка) и & для указания на блок кода. См. perldoc perlsub для полной документации.

Проблемы с прототипами

Прототипы изменяют то, как Perl парсит ваш код, и могут вызывать приведение типов аргументов. Они не документируют количество или типы аргументов, которые функция ожидает, как и не устанавливают соответствие аргументов именованным параметрам.

Приведение типов при использовании прототипов работает неочевидным образом, как навязывание скалярного контекста входным аргументам:

    sub numeric_equality($$)
    {
        my ($left, $right) = @_;
        return $left == $right;
    }

    my @nums = 1 .. 10;

    say 'They're equal, whatever that means!'
        if numeric_equality @nums, 10;

…но работает только с простыми выражениями:

    sub mypush(\@@);

    # ошибка компиляции: несоответствие прототипов
    # (ожидался массив, получено скалярное присваивание)
    mypush( my $elems = [], 1 .. 20 );

Чтобы отладить это, пользователи mypush должны знать как то, что прототип существует, так и ограничения прототипов массивов. Хуже того, это простые ошибки, которые могут вызывать прототипы.

Хорошее использование прототипов

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

Во-первых, они могут позволить вам переопределить встроенные функции. Сначала проверьте, что вы можете переопределить встроенную функцию, проверив её прототип в маленькой тестовой программе. Затем используйте прагму subs, чтобы сказать Perl, что вы собираетесь переопределить встроенную функцию, и, наконец, объявите ваше переопределение с корректным прототипом:

    use subs 'push';

    sub push (\@@) { ... }

Имейте ввиду, что прагма subs действует для всей оставшейся части файла, независимо от лексической области видимости.

Вторая причина использования прототипов — это определение констант времени компиляции. Когда Perl встречает функцию, объявленную с пустым прототипом (в противоположность отсутствию прототипа), и эта функция возвращает единственное константное выражение, оптимизатор превратит все вызовы этой функции в константы вместо вызовов функции:

    sub PI () { 4 * atan2(1, 1) }

Весь последующий код будет использовать вычисленное значение Пи на месте голого слова PI или вызова PI(), с учётом области видимости.

Базовая прагма constant справляется с этими деталями за вас. Модуль Const::Fast из CPAN создаёт константные скаляры, которые вы можете интерполировать в строки.

Разумное использование прототипов — это расширение синтаксиса Perl 5 для оперирования анонимными функциями как блоками. CPAN-модуль Test::Exception использует это во благо для предоставления приятного API с отложенными вычислениями (footnote: См. также Test::Fatal). Его функция throws_ok() принимает три аргумента: блок кода для выполнения, регулярное выражение для сравнения со строкой исключения и необязательное описание теста:

    use Test::More tests => 1;
    use Test::Exception;

    throws_ok
        { my $unobject; $unobject->yoink() }
        qr/Can't call method "yoink" on an undefined/,
        'Method on undefined invocant should fail';

Экспортируемая функция throws_ok() имеет протоип &$;$. Его первый аргумент — это блок, который становится анонимной функцией. Второй аргумент — скаляр. Третий аргумент не обязателен.

Внимательные читатели, возможно, заметили отсутствие запятой после блока. Это причуда парсера Perl 5, который ожидает после прототипированного блока пробел, а не оператор запятой. Это недостаток синтаксиса прототипов.

Вы можете использовать throws_ok(), не пользуясь прототипами:

    use Test::More tests => 1;
    use Test::Exception;

    throws_ok(
        sub { my $unobject; $unobject->yoink() },
        qr/Can't call method "yoink" on an undefined/,
        'Method on undefined invocant should fail' );

Последнее хорошее применение прототипов — при определении произвольной именованной функции для использования с sort (footnote: Бен Тилли (Ben Tilly) предложил этот пример.):

    sub length_sort ($$)
    {
        my ($left, $right) = @_;
        return length($left) <=> length($right);
    }

    my @sorted = sort length_sort @unsorted;

Прототип $$ заставляет Perl передавать сортируемые пары в @_. Документация sort предполагает, что это немного медленнее, чем использование глобальных переменных пакета $a и $b, но использование лексических переменных зачастую оправдывает любое снижение скорости.

Эквивалентность методов и функций

Объектная система Perl 5 преднамеренно минималистична (Благословлённые ссылки). Так как класс является пакетом, Perl не делает различий между функцией и методом, хранящимися в пакете. Одна и та же встроенная директива, sub, объявляет и одно, и другое. Документация может прояснить ваши намерения, но Perl охотно диспетчеризует функцию, вызванную как метод. Подобным же образом вы можете вызвать метод как если бы он был функцией — полностью определённой, экспортированной или ссылкой — если вручную передадите свой инвокант.

Вызов не того, что нужно, не так, как нужно, вызывает проблемы.

Вызывающая сторона

Рассмотрим класс с несколькими методами:

    package Order;

    use List::Util 'sum';

    ...

    sub calculate_price
    {
        my $self = shift;
        return sum( 0, $self->get_items() );
    }

Если есть объект $o класса Order, следующие вызовы этого метода могут выглядеть эквивалентными:

    my $price = $o->calculate_price();

    # сломано; не использовать
    my $price = Order::calculate_price( $o );

Хотя в этом простом случае они дадут один и тот же вывод, последнее нарушает инкапсуляцию объекта, избегая поиска метода.

Если же $o был бы подклассом или алломорфом (Роли) класса Order, который переопределял бы calculate_price(), обход диспетчеризации метода привёл бы к вызову неверного метода. Любое изменение реализации calculate_price(), такое как модификация наследования или делегирования через AUTOLOAD() — могло бы сломать вызывающий код.

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

    my $meth_ref = $o->can( 'apply_discount' );

Здесь есть две возможности. Первая — отбросить возвращаемое значение метода can():

    $o->apply_discount() if $o->can( 'apply_discount' );

Вторая — использовать саму ссылку посредством синтаксиса вызова метода:

    if (my $meth_ref = $o->can( 'apply_discount' ))
    {
        $o->$meth_ref();
    }

Если $meth_ref содержит ссылку на функцию, Perl вызовет эту ссылку с $o в качестве инвоканта. Это работает даже со включенным strict, как и при вызове метода с помощью скаляра, содержащего его имя:

    my $name = 'apply_discount';
    $o->$name();

Есть один небольшой недостаток в вызове метода по ссылке; если структура программы изменяется между сохранением ссылки и вызовом по ссылке, ссылка может уже не ссылаться на наиболее соответствующий метод. Если класс Order был изменён так, что Order::apply_discount уже не является правильным методом для вызова, ссылка в $meth_ref не будет обновлена.

Когда вы используете эту форму вызова, ограничьте область видимости ссылки.

Вызываемая сторона

Так как Perl 5 не делает различий между функциями и методами в точке объявления, и так как возможно (хотя и не рекомендуется) вызвать заданную функцию как функцию или метод, возможно написать функцию, которую можно вызывать любым из этих способов. Базовый модуль CGI — главный обвиняемый. Его функции применяют несколько эвристик для определения того, является ли их первый аргумент инвокантом.

Недостатков этого множество. Трудно точно предсказать, какой инвокант потенциально валиден для заданного метода, особенно если вам приходится иметь дело с подклассами. Также труднее создать API, который пользователи не смогут с лёгкостью неправильно использовать, как и бремя документации становится тяжелее. Что случится, если одна часть проекта использует процедурный интерфейс, а другая — объектный?

Если вам необходимо предоставить отдельный процедурный и ОО интерфейс к библиотеке, создайте два отдельных API.

Связывание

Тогда как перегрузка (Перегрузка) позволяет вам настраивать поведение классов и объектов для конкретных случаев приведения типа, механизм, называемый связыванием, позволяет вам настраивать поведение простых переменных (скаляров, массивов, хешей и дескрипторов файлов). Любая операция, которую вы можете выполнить на связанной переменной, транслируется в определённый вызов метода.

Встроенная директива tie изначально позволяла вам использовать место на диске как резервную память для хешей, так что Perl мог осуществлять доступ к файлам, которые больше, чем можно поместить в память. Базовый модуль Tie::File предоставляет похожую систему и позволяет вам обращаться с файлами как если бы они были массивами.

Класс, с которым вы связываете переменную с помощью tie(), должен соотвествовать определённому интерфейсу для конкретных типов данных. Смотрите perldoc perltie для получения общего представления, затем обратитесь к базовым модулям Tie::StdScalar, Tie::StdArray и Tie::StdHash за более конкретными деталями. Начните с наследования от одного из этих классов, затем переопределите любые конкретные методы, которые вам нужно модифицировать.

Как будто бы tie уже не достаточно сбивающий с толку, Tie::Scalar, Tie::Array и Tie::Hash определяют необходимые интерфейсы для связывания скаляров, массивов и хешей, но реализацию по умолчанию предоставляют Tie::StdScalar, Tie::StdArray и Tie::StdHash.

Связывание переменных

Так можно связать переменную:

    use Tie::File;
    tie my @file, 'Tie::File', @args;

Первый аргумент — это переменная для связывания, второй — имя класса, с которым её нужно связать, а @args — необязательный список аргументов, требуемый для связывающей функции. В случае Tie::File это допустимое имя файла.

Связывающие функции напоминают конструкторы: TIESCALAR, TIEARRAY(), TIEHASH() или TIEHANDLE() для скаляров, массивов, хешей и дескрипторов файлов соответственно. Каждая функция возвращает новый объект, представляющий связанную переменную. Обе встроенные директивы, tie и tied, возвращают этот объект. Однако, большинство использует tied в булевом контексте.

Реализация связанных переменных

Чтобы реализовать класс связанных переменных, унаследуйте от встроенного модуля, такого как Tie::StdScalar (footnote: У Tie::StdScalar отсутствует собственный файл .pm, так что используйте Tie::Scalar, чтобы сделать его доступным.), затем переопределите конкретные методы для операций, которые вы хотите изменить. В случае связанного скаляра это, скорее всего, будут FETCH и STORE, возможно, TIESCALAR(), и, вероятно, не DESTROY().

Вы можете создать класс, который логирует все операции чтения и записи скаляра, с помощью очень небольшого количества кода:

    package Tie::Scalar::Logged
    {
        use Modern::Perl;

        use Tie::Scalar;
        use parent -norequire => 'Tie::StdScalar';

        sub STORE
        {
            my ($self, $value) = @_;
            Logger->log("Storing <$value> (was [$$self])", 1);
            $$self = $value;
        }

        sub FETCH
        {
            my $self = shift;
            Logger->log("Retrieving <$$self>", 1);
            return $$self;
        }
    }

    1;

Допустим, метод log() класса Logger принимает строку и количество фреймов вверх по стеку вызовов, которые нужно вывести.

Внутри методов STORE() и FETCH(), $self работает как благословлённый скаляр. Присваивание этой ссылке на скаляр изменяет значение скаляра, а чтение из неё возвращает его значение.

Аналогично, методы Tie::StdArray и Tie::StdHash воздействуют на благословлённые ссылки на массив и хеш сооветственно. Документация в perldoc perltie объясняет обширное количество методов, которые они поддерживают, как например, вы можете читать или записывать множественные значения в них, помимо других операций.

Опция -norequire предотвращает прагму parent от попытки загрузить файл для Tie::StdScalar, поскольку этот модуль является частью файла Tie/Scalar.pm.

Когда использовать связанные переменные?

Связанные переменные выглядять как весёлые возможности для проявления ума, но они могут приводить к сбивающим с толку интерфейсам. Если у вас нет очень хорошей причины для того, чтобы заставлять объект вести себя так, как если бы он был встроенным типом данных, избегайте создания собственных связываний. К тому же, tie намного медленнее, чем использование встроенных типов данных, ввиду различных причин, кроющихся в реализации.

Хорошие причины включают облегчение отладки (используйте логгируемый скаляр, чтобы понять, где изменяется значение) и необходимость сделать некоторые невозможные операции возможными (доступ к большим файлам способом с эффективным использованием памяти). Связанные переменнные менее полезны как первичные интерфейсы к объектам; зачастую слишком трудно и ограничивающе пытаться вместить весь ваш интерфейс в тот, что поддерживается tie().

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