59. Действия (actions) в грамматиках, часть 3

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

Заодно чуть усложним грамматику и научим ее парсить даты, записанные в виде 28.02.18:

token TOP {
    | <year> <sep> <month> <sep> <day>
    | <day>  <sep> <month> <sep> <year>
    | <n>    <sep> <month> <sep> <nn>
 }

. . .

token n {
    \d ** 1..2
}
token nn {
    \d ** 2
}

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

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

class DateStrActions {
    has %date;

    method TOP($/) {
        if %date<nn> {
            if %date<nn> > 35 {
                %date<year> = 1900 + %date<nn>;
            }
            else {
                %date<year> = 2000 + %date<nn>;
            }
            %date<day> = %date<n>;
        }
        printf "--> %4i-%02i-%02i\n", 
               %date<year>, %date<month>, %date<day>
    }
    method year($/) {
        %date<year> = ~$/;
    }
    method month($/) {
        %date<month> = ~$/;
    }
    method day($/) {
        %date<day> = ~$/;
    }
    method nn($/) {
        %date<nn> = ~$/;
    }
    method n($/) {
        %date<n> = ~$/;
    }
}

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

Теперь самое важное. Поскольку в классе появился атрибут, компилятору потребуется выделить под него место. Для этого от вас требуется создать объект, а не просто передать имя класса:

my $r = DateStr.parse($t, :actions(DateStrActions.new()));

Тестируем код на разных датах:

$ perl6 actions2.pl
2018-02-26 --> 2018-02-26
2019-3-27  --> 2019-03-27
28.04.2020 --> 2020-04-28
30.05.21   --> 2021-05-30
15.06.99   --> 1999-06-15

Полный код вы этого варианта программы можете найти на гитхабе.

58. Действия (actions) в грамматиках Perl 6, часть 2

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

class DateStrActions {
    method TOP($/) {
        printf "--> %4i-%02i-%02i\n", $<year>, $<month>, $<day>
    }
}

Методы этого класса должны совпадать с названиями правил и токенов в грамматике. Из вчерашнего примера я убрал код из токена TOP грамматики DateStr и поместил его в метод TOP класса DateStrActions.

Теперь необходимо сообщить о действиях перед парсингом:

my $r = DateStr.parse($t, :actions(DateStrActions));

Все остальное остается неизменным. Программа печатает тот же результат:

2018-02-26 --> 2018-02-26
2018-02-26 --> 2018-02-26
2018-2-26  --> 2018-02-26
26.02.2018 --> 2018-02-26

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

57. Действия (actions) в грамматиках Perl 6, часть 1

Мы уже видели, как сделать грамматику для разбора чисел. Но сами по себе грамматики дают лишь ответ, удовлетворяет ли строка заданным правилам. Обычно еще требуется что-то сделать с имеющимися данными. Для этого к грамматике надо добавить действия (actions).

Рассмотрим простейшее действие на примере грамматики для разбора даты.

grammar DateStr {
    token TOP {
        [
        | <year> <sep> <month> <sep> <day>
        | <day>  <sep> <month> <sep> <year>
        ] {
            printf "--> %4i-%02i-%02i\n", 
                   $<year>, $<month>, $<day>
        }
    }
    token year {
        \d ** 4
    }
    token month {
        \d ** 1..2
    }
    token day {
        \d ** 1..2
    }
    token sep {
        <[-./]>
    }
}

my @tests = <
    2018-02-26
    2018-2-26
    26.02.2018
>;

for @tests -> $t {    
    print "$t\t";
    my $r = DateStr.parse($t);
}

Грамматика разрешает один из двух видов формата даты: YYYY-MM-DD или DD-MM-YYYY. Разделителем может быть дефис или точка.

Как только найдено соответствие, Perl 6 выполняет блок кода — в нашем примере он выделен цветом.

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

Внутри действия совпавшие части доступны как элементы объекта $/, например, $<year>.

Программа успешно разбирает все три примера и печатает даты в едином формате:

2018-02-26 --> 2018-02-26
2018-02-26 --> 2018-02-26
2018-2-26  --> 2018-02-26
26.02.2018 --> 2018-02-26

56. Что такое gist в Perl 6

Когда вы пробуете печатать объект, например: say $x, Perl 6 вызывает метод gist. Этот метод определен для всех встроенных типов — где-то он вызывает метод Str, где-то perl, а где-то формирует особое представление.

Рассмотрим, как можно воспользоваться этим методом, чтобы сделать свой вариант печати объекта:

class X {
    has $.value;

    method gist {
        '[' ~ $!value ~ ']'
    }
}

my $x = X.new(value => 42);

say $x; # [42]
$x.say; # [42]

При обращении к say программа печатает число в квадратных скобках: [42].

Обратите внимание, что при интерполяции в строке, заключенной в двойные кавычки, вызывается другой метод — Str:

say $x.Str; # X<140586830040512>
say "$x";   # X<140586830040512>

Если вам требуется кастомная интерполяция, переопределяйте и метод Str:

class X {
    has $.value;

    method gist {
        '[' ~ $!value ~ ']'
    }
    method Str {
        '"' ~ $!value ~ '"'
    }
}

my $x = X.new(value => 42);

say $x;     # [42]
$x.say;     # [42]

say $x.Str; # "42"
say "$x";   # "42"

 

55. Perl 6 на Raspberry Pi и другие дистрибутивы

Вчера я пробовал установить Rakudo Perl 6 на Orange Pi. Сегодня пробуем Raspberry Pi (третьей версии).

Raspberry Pi

На этот раз все получилось. Я скачал последние исходники Rakudo Star и действовал по инструкции:

perl Configure.pl --backend=moar --gen-moar
make
make install

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

Вот выдержка из лога:

Stage start      :   0.000
Stage parse      : 767.004
Stage syntaxcheck:   0.000
Stage ast        :   0.000
Stage optimize   : 107.122
Stage mast       : 198.105
Stage mbc        :   8.091

На обычном ноутбуке таких цифр вы не увидите, там все компилируется за минуту-две.

Довольно долгим был и процесс после make install. Видимо, если ставить не Rakudo Star, а просто Rakudo, то все пройдет быстрее, потому что для Rakudo Star устанавливаются и тестируются дополнительные модули.

Но тем не менее, хоп-хоп, запускаем (не дожидаясь окончания действий с модулями):

pi@rpi:~/rakudo-star-2018.01 $ ./perl6 -v
This is Rakudo Star version 2018.01 built on MoarVM version 2018.01
implementing Perl 6.c.

Для других ОС

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

  • Alpine 3.6 x86_64
  • Alpine 3.7 x86_64
  • Centos 7 x86_64
  • Debian 8 amd64
  • Debian 9 amd64
  • Fedora 25 x86_64
  • Fedora 26 x86_64
  • Fedora 27 x86_64
  • openSUSE 42.3 x86_64
  • Ubuntu 14.04 amd64
  • Ubuntu 16.04 amd64
  • Ubuntu 17.10 amd64
  • Ubuntu 16.04 i386
  • Ubuntu 17.10 i386

 

54. Perl 6 на Orange Pi

Сегодня мы выйдем за пределы одного чистого программирования и возьмем в руки паяльник. Задача на вечер — помигать светодиодом на Orange Pi.

Orange Pi — это одноплатный мини-компьютер, который, видимо, появился как конкурент Raspberry Pi, при этом и меньше размером, и дешевле. За 15 евро вы получаете четырехядерный компьютер размером со спичечный коробок, куда можно поставить Линукс. Ну не чудо ли? Да еще и c Wi-Fi.

Почти все сказанное ниже применимо, разумеется, и к Raspberry Pi.

Для экспериментов я установил дистрибутив ARMBIAN Debian 9, Rakudo Perl 6 и WiringOP (ну еще пришлось припаять планку с контактами).

WiringOP — это библиотека и утилиты для работы с портами ввода-вывода. Устанавливается просто:

git clone https://github.com/zhaolei/WiringOP.git -b h3
 
cd WiringOP
chmod +x ./build
sudo ./build

С перлом же пришлось повозиться. Я хотел скомпилировать свежий Rakudo Star и начал делать все по инструкции. Началось все замечательно, но потом намертво встало, нагрев процессор до 60-70 градусов. При этом невозможно было достучаться до компьютера по SSH, хотя видно было, что оно просто дико тормозит, но работает.

В итоге я не дождался, пока все соберется, и пошел искать готовые порты. Нашлись два имени — perl6 и rakudo — и это дистрибутив версии 2016.12.

# apt-get install perl6

Конечно, декабрь 2016 это уже очень старое, но при этом версия языка 6.c, что в целом нормально: эта текущая версия спецификации Perl 6.

This is Rakudo version 2016.12 built on MoarVM version 2016.12
implementing Perl 6.c.

Теперь пишем программу на Perl 6:

my $pin = 7;
shell("gpio mode $pin OUT");

for True, !* ... * {
    shell("gpio write $pin " ~ +$_);
    sleep 0.5;
}

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

Обратите внимание на интересные моменты в программе.

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

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

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

В-четвертых, не забывайте, что sleep в Perl 6 может принимать и нецелые значения.

53. Функция EVAL в Perl 6

Для выполнения кода, сохраненного в строке, в Perl 6 есть функция EVAL. Именно так, большими буквами, чтобы не было похоже на Perl 5. В работе функции тоже есть отличия от старой eval.

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

Простейший пример:

EVAL('say 123');

Программа ожидаемо печатает 123.

Пример чуть посложнее:

my $var = 42;
EVAL('say $var'); # 42

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

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

my $var = 42;
EVAL("say $var");

Эта программа не работает:

===SORRY!=== Error while compiling eval3.pl
EVAL is a very dangerous function!!! (use the MONKEY-SEE-NO-EVAL pragma to override this error,
but only if you're VERY sure your data contains no injection attacks)
at eval3.pl:2
------> EVAL("say $var")⏏;

Нужно добавить специальную инструкцию. Либо так:

use MONKEY-SEE-NO-EVAL;

my $var = 42;
EVAL("say $var");

Либо так:

use MONKEY;

my $var = 42;
EVAL("say $var");

Теперь программа, которая будет выполнена внутри EVAL, полностью эквивалентна такой программе:

EVAL('say 42');

Исполнение блоков кода по умолчанию тоже не разрешено. То есть, следующая программа тоже потребует явного упоминания MONKEY:

use MONKEY;

EVAL("say {1 + 2 + 3}"); # 6

Обратите внимание, что EVAL не следует применять вместо try.

52. Пара слов о промисах в Perl 6

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

my $promise = Promise.new;

start {
    sleep 3;
    $promise.keep;
}

for 1..* {
    .say;
    sleep 1;    
    last if $promise;
}

Сначала создается объект типа Promise, он сохраняется в одноименной переменной $promise. Затем создается отдельный поток, который через три секунды делает промис (обещание) выполненным:

$promise.keep;

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

last if $promise;

В итоге программа печатает числа от одного до трех:

$ perl6 promise.pl 
1
2
3

После создания промис находится в состоянии Planned. Это одно из возможных состояний. Два других — обещание выполнено или нарушено: Kept или Broken.

Посмотреть текущий статус можно с помощью метода status:

my $promise = Promise.new;
say $promise.status; # Planned

Методы keep и break изменяют состояние промиса, например:

$promise.break; 
say $promise.status; # Broken

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

last if $promise.status eq 'Kept';

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