Выпуск 13. Март 2014

«Старые» современные возможности Perl | Содержание | Обзор CPAN за февраль 2014 г.

Сигнатура функции в Perl 5.20

Случилось то, что многие так долго ждали. 6 февраля 2014 г. реализация сигнатуры функции была добавлена в основную ветку разработки Perl и стала доступна для использования всем желающим. Эта экспериментальная возможность включена в релиз для разработчиков 5.19.9, и, если не будет выявлено серьёзных проблем, войдёт в грядущий стабильный релиз Perl 5.20.

Пурпурные сигнатуры

Попытки добавить сигнатуру функции в ядро Perl предпринимаются уже достаточно давно. Peter Martini демонстрировал сообществу наработки ещё до релиза 5.18, но тогда эта реализация была достаточно сырой и имела множество недостатков. Работа была продолжена и, например, в версии 5.19.5 появилась альтернативная возможность для указания прототипа функции через специальный атрибут функции prototype. Это был небольшой шаг к продвижению реализации сигнатур в ядре Perl.

В середине января 2014 г. по результатам дискуссии в списке рассылки p5p стало казаться, что сигнатуры функции опять не попадут в ядро и будут отложены на неопределённое время. Zefram выступил с разгромной критикой на реализацию Питера, доказывая, что предложенное изменение API крайне нежелательно. Но, как известно, разговоры ничего не стоят, поэтому Zefram продемонстрировал свой вариант реализации сигнатур, который вообще не меняет существующее API, лишь расширяя существующий синтаксис прототипов в случае если эта возможность явно включена.

Первоначально реализация получила название simple_signatures (простые сигнатуры), но поскольку обсуждение выявило неподдельный интерес к реализации сигнатур с возможностью для расширения синтаксиса, то новая реализация получила кодовое название purple_signatures (пурпурные сигнатуры): с одной стороны из-за того, что Zefram’у трудно было подобрать подходящее прилагательное, с другой стороны, возможно потому, что пурпурный цвет часто относят к роскоши (цвет одежды римских императоров, церковных служителей и т.д.), таким образом закладывается база для построения совершенной и законченной системы сигнатур функции, которой так не хватало в Perl.

Ricardo Signes, как текущий ответственный за выпуск стабильных версий Perl, одобрил включение пурпурных сигнатур в основную ветку, тем самым закрепив реализацию сигнатур Zefram’а для будущих версий Perl. Ну а реализация Питера осталась не у дел: несмотря на его попытки переделать решение с исправлением всех высказанных замечаний, ему достаточно твёрдо сказали, что он может и дальше присылать свои патчи для Perl, но уже в рамках какой-либо другой темы.

Использование сигнатуры функции

Для того, чтобы поэкспериментировать с новыми сигнатурами, достаточно установить Perl 5.19.9 или blead:

$ perlbrew install blead

Для использования этой сборки Perl в текущей шел-сессии выполните:

$ perlbrew use blead

Функционал сигнатур является на данный момент экспериментальным и активируется путём подключения возможности signatures. Таким образом, каждая программа, использующая эту возможность должна начинаться со следующих строк:

use feature 'signatures';
no warnings 'experimental::signatures';

Простейший пример сигнатуры функции

sub sum ($x, $y) {
    return $x + $y
}

my $z = sum(2,2); # 4

Определяется, что функция sum() имеет два аргумента: переменные $x и $y. Функция не может быть вызвана с меньшим или большим числом аргументов — это приведёт к ошибке во время исполнения.

Это становится наглядно, если посмотреть на код под призмой B::Deparse:

sub sum {
    use feature 'signatures';
    die 'Too many arguments for subroutine' unless @_ <= 2;
    die 'Too few arguments for subroutine' unless @_ >= 2;
    my $x = $_[0];
    my $y = $_[1];
    ();
    return $x + $y;
}

Как видно, переменные $x, $y являются копиями переданных в функцию аргументов. В то время как массив @_ содержит алиасы на аргументы функции.

Необязательные аргументы

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

sub sum ( $x, $y = 1 ) {
    return $x + $y
}

my $z = sum(1);  # $z = 2

Таким образом, если не указан второй аргумент, то он принимает значение по-умолчанию: 1.

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

# Ошибка! Обязательный аргумент идёт после необязательного
sub sum ( $x = 0, $y ) {
     return $x + $y
}

Кроме того, значение undef имеет приоритет над значением по умолчанию:

sub sum ( $x, $y = 10) {
    return $x + $y
}

my $z = sum(5, undef);  # $z = 5, а не 15

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

sub sum ($x, $y = $x + 5) {
    return $x + $y

my $z = sum(10); # $z = 10 + (10 + 5) = 25

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

use strict;

# Ошибка! $z ещё не определена при первом использовании
sub sum ($x, $y = $x+$z+1, $z = 10) {
     return $x + $y + $z
}

Как видно в примере, при задании значения по умолчанию для переменной y, переменнаяz ещё не определена.

Переменное число аргументов

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

sub sum ($x, $y, @other) {
    my $z = 0;
    $z += $_ for @other;
    return $x + $y + $z
}

my $value = sum(1,2,3,4,5,6,7)   # $value = 28

Также возможно использование хеша:

sub method ($self, %opts) {
    ...
}

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

Игнорирование некоторых аргументов

Иногда может потребоваться пропустить часть аргументов (неважных для данной функции) и не выделять под их хранение локальную переменную. Для этого можно использовать обычный сигил без имени переменной в сигнатуре:

sub sum_1_4( $x, $, $, $y, @) {
    return $x + $y
}

sum_1_4( 1, "not important", "not important", 2, "bla bla", "foo bar"); # 3

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

Прототип функции

Если используются сигнатуры, то для описания прототипа функции должен использоваться атрибут prototype. Например:

sub foo :prototype($$) ($x, $y) {
    ...
}

Прототип должен быть указан до сигнатуры, т.к. после сигнатуры функции может идти только тело функции.

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

Зачем могут потребоваться прототипы, если есть сигнатуры функции? Рассмотрим простой пример, где эти отличия проявляются:

sub sum_np ($x, $y = 40) {
    return $x + $y
}

sub sum_wp :prototype($;$) ($x, $y = 40) {
    return $x + $y
}

my @arr = (10, 14);
say sum_np(@arr);  # 24
say sum_wp(@arr);  # 42

Две одинаковые функции отличаются прототипом. В случае без прототипа мы передаём значения $x и $y в массиве @arr — это числа 10 и 14, их сумма равна 24. В случае с прототипом мы ожидаем первым параметром скаляр, таким образом массив @arr интерпретируется в скалярном контексте и возвращает 2 — число элементов массива. Второе значение не передаётся, поэтому переменная $y получает значение по умолчанию 40 и итоговая сумма становится 42.

Переменная @_

Большую дискуссию вызвал вопрос, что делать с массивом @_, содержащим переданные в функцию параметры. Одной из предпосылок создания сигнатуры функции была идея оптимизации, чтобы не инициализировать переменную @_. Однако текущая реализация никак не затрагивает переменную @_, более того выносит отказ от @_ за рамки реализации сигнатуры функции.

Высказывалось несколько идей оптимизации, например, если функция не использует переменную @_, то и не надо инициализровать её. Но, к сожалению, парсер не может выявить использование @_ в подпрограммах на этапе компиляции, т.к. некоторые части программы могут определяться только на этапе исполнения. Например, при выполнении eval кода, содержащегося в строке:

sub foo {
    eval 'say for @_';
}

Dave Mitchell также привёл пример с выполнением кода в регулярных выражениях:

$pat = qr/(?{ print "[$_]\n" for @_ })/;
sub f($str, $pat) { $str =~ $pat }

Поэтому пока переменная @_ по-прежнему доступна в функциях с сигнатурой.

Заключение

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

Владимир Леттиев


«Старые» современные возможности Perl | Содержание | Обзор CPAN за февраль 2014 г.
Нас уже 1393. Больше подписчиков — лучше выпуски!

Комментарии к статье