31. Грамматики в Perl 6, часть 1. Разбор чисел

Грамматики (grammars) в Perl 6 — огромная бесконечная тема, не имеющая аналогов в других языках программирования. Эта часть языка отлично проработана и используется для парсинга самого Perl 6.

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

Для начала создадим набор тестовых данных:

my @tests = <
     1
     -1
     +1
     123
     -123
     1.2
     -1.2
     10000000
     -10000000
     .23
     -.23
     1e2
     1E2
     1e-2
     1e+2
     -1E-2
     1.2E3
     .2E3
     -.2E3
>;

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

grammar Number {
    token TOP {
        <number>
    }
    token number {
        <digit>+
    }
}

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

Пройдемся по тестовым строкам и разберем их с помощью существующей грамматики:

for @tests -> $value {
    my $result = Number.parse($value);
    my $check = $result ?? '✓' !! '✗';
    say "$check $value";
}

Запускаем программу и смотрим на результаты:

✓ 1
✗ -1
✗ +1
✓ 123
✗ -123
✗ 1.2
✗ -1.2
✓ 10000000
✗ -10000000
✗ .23
✗ -.23
✗ 1e2
✗ 1E2
✗ 1e-2
✗ 1e+2
✗ -1E-2
✗ 1.2E3
✗ .2E3
✗ -.2E3

Есть еще над чем поработать. Начнем с добавления необязательного знака:

grammar Number {
    token TOP { 
        <number>
    }
    token number {
        <sign>?
        <digit>+
    }
    token sign {
        '+' | '-'
    }
}

Число успешных тестов немного увеличилось:

✓ 1
✓ -1
✓ +1
✓ 123
✓ -123
✗ 1.2
✗ -1.2
✓ 10000000
✓ -10000000
✗ .23
✗ -.23
✗ 1e2
✗ 1E2
✗ 1e-2
✗ 1e+2
✗ -1E-2
✗ 1.2E3
✗ .2E3
✗ -.2E3

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

grammar Number {
    token TOP { 
        <sign>?
        <number>
    }
    token number { 
        | <comma> <fractional>
        | <integer> <comma> <fractional>
        | <integer> <comma>
        | <integer>
    }
    token sign {
        '+' | '-'
    }
    token integer {
        <digit>+
    }
    token fractional {
        <digit>+
    }
    token comma {
        '.'
    }
}

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

✓ 1
✓ -1
✓ +1
✓ 123
✓ -123
✓ 1.2
✓ -1.2
✓ 10000000
✓ -10000000
✓ .23
✓ -.23
✗ 1e2
✗ 1E2
✗ 1e-2
✗ 1e+2
✗ -1E-2
✗ 1.2E3
✗ .2E3
✗ -.2E3
✓ 1.
✓ -2.

Отлично. Добавляем правила для разбора научной записи:

grammar Number {
    token TOP { 
        <sign>?
        <number>
        [
            ['e' | 'E'] <sign>? <integer>
        ]?
    }
    token number { 
        | <comma> <fractional>
        | <integer> <comma> <fractional>
        | <integer> <comma>
        | <integer>
    }
    token sign {
        '+' | '-'
    }
    token integer {
        <digit>+
    }
    token fractional {
        <digit>+
    }
    token comma {
        '.'
    }
}

Проверка показывает, что все тесты успешно проходят:

✓ 1
✓ -1
✓ +1
✓ 123
✓ -123
✓ 1.2
✓ -1.2
✓ 10000000
✓ -10000000
✓ .23
✓ -.23
✓ 1e2
✓ 1E2
✓ 1e-2
✓ 1e+2
✓ -1E-2
✓ 1.2E3
✓ .2E3
✓ -.2E3
✓ 1.
✓ -2.

На сегодня это все. Созданная грамматика смогла разобрать все запланированные варианты. План на следующий раз — дополнить грамматику действиями (actions), чтобы разобранную строку превратить в полноценное число.

30. Цепочки последовательностей в Perl 6

На днях в листе рассылке perl6-users был интересный пример, которым мне бы хотелось с вами поделиться.

Речь идет о том, что возможно объединять в цепочку оператор ... (так называемый sequence operator).

Давайте сначала посмотрим, что значит объединять операторы в цепочку. Два классических примера — сложное условие и оператор редукции.

Условия, содержащие одну и ту же переменную, можно объединять в цепочку, то есть вместо 1 < $x && $x < 10 писать 1 < $x < 10:

my $x = 5;
say 'Ok' if 1 < $x < 10;
say 'Ok' if 1 < $x && $x < 10;

Оператор редукции позволяет поставить оператор между отдельными элементами списка. Следующие две строки эквивалентны:

say [+] 1, 2, 3;
say 1 + 2 + 3;

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

say 1...10; # (1 2 3 4 5 6 7 8 9 10)

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

say 1...5...1; # (1 2 3 4 5 4 3 2 1)

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

say 1...5...1...4...2...5; 
# (1 2 3 4 5 4 3 2 1 2 3 4 3 2 3 4 5)

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

say 1, 2, 4 ... 16; # (1 2 4 8 16)

Это свойство не теряется при объединении в цепочку:

say 1, 2, 4 ... 16, 15 ... 10; 
# (1 2 4 8 16 15 14 13 12 11 10)

Кастомные правила тоже сохраняют работоспособность:

say 1, 1, * + * ... 13; 
# (1 1 2 3 5 8 13)

say 1, 1, * + * ... 13, 1, * + * ... 44; 
# (1 1 2 3 5 8 13 1 14 15 29 44)

Йоху.

29. Как поменять местами два значения в Perl 6

Разумеется, нас интересует возможность обмена значениями без привлечения третьей временной переменной.

В Perl 6 это можно сделать ровно так же как и в Perl 5:

my $a = 10;
my $b = 20;

($a, $b) = ($b, $a);

say "$a, $b"; # 20, 10

Скобки здесь обязательны, без них не получится.

Есть и еще один вариант:

my $a = 10;
my $b = 20;

($a, $b).=reverse;

say "$a, $b"; # 20, 10

Здесь ($a, $b) — объект типа List. Вызванный на нем метод reverse обращает список. Но метод вызван не как обычно, а через постфиксный псевдо-оператор .=.

Семантика вызова $obj.=method отличается от $obj.method точно так же как $i += 1 отличается от $i + 1. То есть результат, возвращаемый методом, присваивается списку, на котором метод был вызван. В нашем случае был анонимный список, состоящий из двух переменных, поэтому они и получат новые значения.

Аналогичным способом можно обращать и более длинные списки и массивы. Например:

my @a = 1..10;
@a.=reverse;
say @a; # [10 9 8 7 6 5 4 3 2 1]

28. Немного о типе Num в Perl 6

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

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

my $n = Num.new(42);
say $n.WHAT; # (Num)

my $m = 4.2E2;
say $m.WHAT; # (Num)

Встроенные константы типа числа пи — тоже имеют тип Num:

say pi.WHAT;  # (Num)
say e.WHAT;   # (Num)
say Inf.WHAT; # (Num)

Тип результата арифметических операций зависит от типов операндов. Например, если в вычислениях участвуют только рациональные числа, результат будет рациональным. Если же встретится число с плавающей точкой, получится Num:

say (0.1 + 0.2).WHAT;   # (Rat)
say (1e-1 + 2e-1).WHAT; # (Num)
say (0.1 + 1E-1).WHAT;  # (Num)

При вычислениях с величинами, которые не могут быть точно представлены в виде 64-битового числа с плавающей точкой, нужно проявлять осторожность. Например, вчера в Твиттере обсуждался такой пример:

say 5.5e22 % 100; # 0

Понятно, что правильный ответ должен быть 0. И действительно, Perl 6 печатает 0.

Однако, если преобразовать это число к целому числу (Int), то проявится ошибка представления:

say 5.5e22.Int; # 55000000000000002097152

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

say (5.5e22 + 1) % 100; # 0?!

А печатается ноль.

27. Захват в регексах Perl 6

Захватывающие скобки

Регексы Perl 6, как и регулярные выражения в Perl 5, захватывают совпавшие подстроки, заключенные в круглые скобки. Например:

'Hello, World!' ~~ / ( . ) ',' /;
say $0;

Регекс захватил символ, расположенный перед запятой:

「o」

На что следует обратить внимание. Во-первых, переменные нумеруются, начиная с нуля, а не с единицы: $0. Во-вторых, в такой переменной находится объект типа Match, а не просто строка:

say $0.WHAT; # (Match)

Преобразование в строку можно выполнить с помощью префиксного унарного оператора ~:

say (~$0).WHAT; # (Str)

Именованный захват

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

for 'Hello, World!', 'Hi, John!' -> $str {
    $str ~~ /
        $<greeting>=(Hello | Hi)
        ', '
        $<name>=(\w+)
    /;

    say $<greeting>;
    say $<name>;
}

Здесь приветствие сохраняется в переменной $<greeting>, а имя — в $<name>. Такая запись — сокращенная форма полного обращения к полям переменной типа Match: $/<greeting> или $/<name>.

Незахватывающие скобки

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

say 'Hello, World!' ~~ / [Hello | Hi] /; # 「Hello」
say 'Hi, World!'    ~~ / [Hello | Hi] /; # 「Hi」

 

26. Что такое soft failure в Perl 6

В Perl 6 есть понятие soft failure — это исключения, которые проявляются не сразу, а только тогда, когда они уже неизбежны.

Пример 1

Типичный пример такой ситуации — деление на ноль.

my $x = 42;
my $y = $x / 0;
say 'Okay?';

Запускаем:

$ perl6 div0.pl 
Okay?

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

Однако, если попытаться напечатать значение $y, ошибка проявится.

my $x = 42;
my $y = $x / 0;
say "\$y = $y";

В этом случае программа завершится с исключением:

$ perl6 div0.pl 
Attempt to divide 42 by zero using div
  in block <unit> at div0.pl line 4

Пример 2

Второй пример — открытие несуществующего файла. Вот простейшая программа:

my $f = open 'rubbish-name.txt';
say 'Okay?';

Если ее запустить, ничего страшного не случится:

$ perl6 file0.pl 
Okay?

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

$ perl6 file0.pl 
Failed to open file /Users/ash/rubbish-name.txt: No such file or directory
  in block <unit> at file0.pl line 1

25. Альтернативы в регексах Perl 6

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

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

say 'abcd' ~~ / a | ab | abc /;

Программа печатает 「abc」, то есть совпала самая длинная строка, несмотря на то, что она была последней в списке.

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

say 'abcd' ~~ / a || ab || abc /;

На печати окажется 「a」, то есть первый же совпавший вариант.

В Perl 6 к регексам или их частям можно добавить блок кода, который выполнится, если эта часть совпала. Модифицируем предыдущие примеры:

'abcd' ~~ / 
    | a   { say 'a'   }
    | ab  { say 'ab'  }
    | abc { say 'abc' }
/;

'abcd' ~~ /
    || a   { say 'a'   }
    || ab  { say 'ab'  }
    || abc { say 'abc' }
/;

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

Программа печатает две строки:

abc
a

То есть был выполнен только тот блок кода, который соответствует выбранной альтернативе. Во втором примере это очевидно, а в первом — хотя последовательно совпадают и a, и ab, выполняется только третий блок кода.

24. Приблизительное сравнение в Perl 6

В Perl 6 есть оператор приблизительного равенства (approximately-equal operator). Он существует в двух формах — ASCII =~= и юникодном .

Оператор возвращает истину, если относительная разность между операндами меньше величины $*TOLERANCE, которая по умолчанию равна 10–15.

say 3.14159265358979323 =~= pi; # True
say 3.14 =~= pi;                # False

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

{
    my $*TOLERANCE = 0.1;
    say pi =~= 3.14; # True
}

Пара слов о работе оператора. Если оба операнда отличны от нуля, происходит такое сравнение:

|$a - $b| / max(|$a|, |$b|) < $*TOLERANCE

(Здесь |$a| — абсолютное значение величины.)

Если же один из операндов — ноль, то в этом случае возвращается результат сравнения абсолютной величины второго операнда с нулем. То есть в этом случае второй операнд не должен превышать по модулю 10–15:

say 1E-14 =~= 0; # False
say 1E-16 =~= 0; # True