Как создать подпрограмму Perl, которая принимает блок кода

#perl #arguments #subroutine

#perl #аргументы #подпрограмма

Вопрос:

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

 sub foo_1($) {
  my $name = shift;
  my $f; 

  run_something();
  open($f, $name) or die ("Couldn't open $name");
  while (<$f>) {
    //Something for foo_1()
  }
  close($f); 
  do_something_else();

}
  

И у меня есть примерно четыре или более, которые выглядят одинаково, единственное, что меняется, это тело блока while. Я хотел бы абстрагироваться от этого и прекратить копировать-вставлять код повсюду.

  • Есть ли способ закодировать подпрограмму, которая принимает блок кода и выполняет его?

Чтобы дать больше контекста, разные подпрограммы foo представляют собой разные конечные автоматы (FSM), которые считывают содержимое разных файлов и передают данные в хэш-ссылку. Возможно, есть более разумная вещь, которую можно сделать, чем то, что я пытаюсь выполнить.

Комментарии:

1. Вообще говоря, прототипы функций не нужны, и я нахожу их отчасти бесполезными.

2. @jiggy Не могли бы вы немного уточнить, пожалуйста?

3. => В этом случае использование ($) прототипа может означать не то, что вы думаете. Это действительно означает «дайте мне один аргумент», но это также означает «наложить скалярный контекст на этот аргумент». Итак, если бы у вас был массив с одним элементом в нем, и вы вызывали бы его foo_1 @array , тогда foo_1 было бы передано число 1 , которое является количеством элементов в массиве. Чтобы фактически получить первый аргумент, вам нужно было бы вызвать его как foo_1 $array[0] . Если бы у вас не было прототипа, то вы могли бы вызвать его как foo_1 @array , и это сработало бы должным образом.

4. … Некоторые программисты на Perl называют это наложение контекста «действием на расстоянии», поскольку в самом вызове функции нет ничего, что указывало бы на то, что аргумент будет находиться в скалярном контексте (и это может быть источником трудно обнаруживаемых ошибок). В общем, вам следует ограничить использование прототипа в Perl теми случаями, когда вы хотите написать функцию, которая анализируется как встроенная функция (например, в моем ответе ниже). Проверка аргументов может быть выполнена во время выполнения с помощью строки типа @_ == 1 or die "function takes 1 argument" в верхней части подпрограммы.

5. @Эрик Хорошо! Я действительно хотел наложить «скалярный контекст» на аргумент foo_1 (поскольку в примере единственным аргументом было имя файла), но я не был полностью осведомлен о части «только для функций, подобных встроенным». Спасибо.

Ответ №1:

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

Чтобы сделать это с прототипами, вы используете, sub name (amp;) {...} где amp; указывается perl, что первым аргументом функции является либо блок (с или без sub ), либо литеральная подпрограмма amp;mysub . (amp;) Прототип указывает один и только один аргумент, если вам нужно передать несколько аргументов после блока кода, вы могли бы записать это как, (amp;@) что означает, блок кода, за которым следует список.

 sub higher_order_fn (amp;@) {
    my $code = amp;{shift @_}; # ensure we have something like CODE

    for (@_) {
        $code->($_);
    }
}
  

Эта подпрограмма будет запускать блок «передано в» для каждого элемента списка «Передано в». amp;{shift @_} Выглядит немного загадочно, но то, что она делает, — это смещает первый элемент списка, который должен быть блоком кода. amp;{...} Разыменовывает значение как подпрограмму (вызывая любую перегрузку), а затем немедленно принимает ссылку на него. Если значением была ссылка на КОД, то оно возвращается без изменений. Если это был перегруженный объект, он превращается в код. Если его не удалось принудительно преобразовать в КОД, выдается ошибка.

Чтобы вызвать эту подпрограмму, вы должны написать:

 higher_order_fn {$_ * 2} 1, 2, 3;
# or
higher_order_fn(sub {$_ * 2}, 1, 2, 3);
  

(amp;@) Прототип, который позволяет записывать аргумент в виде map / grep подобного блока, работает только при использовании функции более высокого порядка в качестве функции. Если вы используете ее как метод, вам следует опустить прототип и записать его таким образом:

 sub higher_order_method {
    my $self = shift;
    my $code = amp;{shift @_};
    ...
    $code->() for @_;
}
...
$obj->higher_order_method(sub {...}, 'some', 'more', 'args', 'here');
  

Комментарии:

1. …продолжение (я по ошибке нажал enter) Я хотел бы знать, почему вы используете amp;{shift @_}. Почему это отличается от других ответов, которые мне давали люди? Я довольно новичок в Perl (но я некоторое время кодировал на Haskell, Java, Scheme и C). Спасибо.

2. это краткий способ проверки того, что аргумент на самом деле является ссылкой на код. расширенное это означает что-то вроде do {my $x = shift; ref $x eq 'CODE' ? $x : overload::Overloaded($_[0], 'amp;{}') ? amp;$x : die "not a code reference"} . Прототип — это ограничение времени компиляции, которое проверяет наличие ссылки на код для вас, но его можно обойти, вызвав sub с amp; символом (или вызвав код как метод). amp;{shift @_} Является дополнительной проверкой на этот обход.

3. Я еще пару раз перечитал ваш ответ и теперь понимаю. Спасибо.

4. Отлично! Большое тебе спасибо, Эрик. Ваш комментарий выше не появлялся до тех пор, пока я не написал свой. Это было то, что я себе представлял. Еще раз спасибо!

5. Несмотря на то, что вы немного перепутали свой ответ с функциями управления списками более высокого порядка, я выбрал ваш как лучший. Спасибо.

Ответ №2:

 sub bar {
   my ($coderef) = @_;
   ⁝
   $coderef->($f, @arguments);
   ⁝
}

bar(sub { my ($f) = @_; while … }, @other_arguments);
  

или, возможно, немного менее запутанный с именованным coderef:

 my $while_sub = sub {
    my ($f) = @_;
    while …
    ⁝
};
bar($while_sub, @other_arguments);
  

Редактировать: В книге по Perl более высокого порядка полно такого рода программ.

Комментарии:

1. Большое вам спасибо. Я думаю, что прочитаю эту книгу. Звучит забавно!

Ответ №3:

Вам нужен amp; прототип.

 sub foo(amp;@) {
    my ($callback) = shift;
    ...
    $callback->(...);
    ...
}
  

создает

 foo { ... } ...;
  

эквивалентно

 foo(sub { ... }, ...);
  

Ответ №4:

Хотя другие уже ответили на вопрос, мне все еще не хватало ссылки на официальную документацию Perl.

http://perldoc.perl.org/perlsub.html#Prototypes