Цель этого документа - дать вам краткий обзор языка программирования Raku. Тем, кто только знакомится с Raku, он должен помочь начать им пользоваться.

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

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

Лицензия

Данный документ лицензирован в терминах Creative Commons Attribution-ShareAlike 4.0 International License. Для просмотра копии лицензии, посетите

Содействие

Если вы хотите внести вклад в этот документ, перейдите по ссылке:

Обратная связь

Любые отзывы очень приветствуются:

Если вам понравилось это введение, поставьте "звезду" репозиторию на Github.

Переводы

1. Введение

1.1. Что такое Raku

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

Девиз Raku:
  • TMTOWTDI (читается как "Тим Тоуди"): "Есть больше одного способа сделать это".

  • Простые вещи должны оставаться простыми, сложные - упрощаться, а невозможное должно быть сложным.

1.2. Жаргон

  • Raku: Спецификация языка с тестовым набором (test suite). Реализации, которые проходят все тесты спецификации, считаются Raku.

  • Rakudo: Компилятор Raku.

  • Rakudobrew: Менеджер установки Rakudo.

  • Zef: менеджер модулей Raku.

  • Rakudo Star: Сборка, включающий в себя Rakudo, Zef, набор Raku модулей и документацию.

1.3. Установка Raku

Linux

Чтобы установить Rakudo Star, запустите следующие команды в вашем эмуляторе терминала:

mkdir ~/rakudo && cd $_
curl -LJO https://rakudo.org/latest/star/src
tar -xzf rakudo-star-*.tar.gz
mv rakudo-star-*/* .
rm -fr rakudo-star-*

./bin/rstar install

echo "export PATH=$(pwd)/bin/:$(pwd)/share/perl6/site/bin:$(pwd)/share/perl6/vendor/bin:$(pwd)/share/perl6/core/bin:\$PATH" >> ~/.bashrc
source ~/.bashrc

С другими вариантами можно ознакомиться на https://rakudo.org/star/source

macOS

Есть четыре варианта:

  • Следуйте тем же шагам, что указаны для установки на Linux.

  • Установить с помощью homebrew: brew install rakudo-star

  • Установить с помощью MacPorts: sudo port install rakudo

  • Загрузите последний установщик (файл с расширением .dmg) с https://rakudo.org/latest/star/macos

Windows
  1. Для 64-битной архитектуры: скачайте установщик (файл с расширением .msi) с https://rakudo.org/latest/star/win

  2. После установки убедитесь, что C:\rakudo\bin добавлен в переменную PATH.

Docker
  1. Установите официальный образ Docker docker pull rakudo-star

  2. Затем запустите контейнер с образом docker run -it rakudo-star

1.4. Запуск кода на Raku

Запуск кода Raku может быть осуществлён с помощью REPL ("Read-Eval-Print Loop"", цикл «чтение — вычисление — вывод»). Для этого откройте эмулятор терминала, наберите perl6 или raku в командной строке и нажмите [Enter]. После этого должно появиться приглашение командной строки - символ >. Далее напечатайте строку кода и нажмите [Enter]. REPL выведет значение строки. Затем вы можете напечатать другую строку или напечатать exit и нажать [Enter], чтобы покинуть REPL.

Также вы можете написать код в файле, сохранить его и затем запустить. Рекомендуется сохранять Raku скрипт в файл с расширением .p6. Запустите файл, напечатав perl6 имяфайла.p6 в окне эмулятора терминала и нажав [Enter]. В отличии от использования REPL, результат каждой строки не будет автоматически выводиться: для этого код должен содержать инструкции для вывода, к примеру say.

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

Также отдельные строки кода могут быть запущены в командной строке в не-интерактивном режиме вводом perl6 -e 'ваш код' и нажатием [Enter].

Rakudo Star также содержит интерактивный редактор, который поможет вам пользоваться REPL в полной мере.

Если вы установили только Rakudo вместо Rakudo Star, у вас вероятно не будет определенных возможностей REPL (листание истории клавишами "вверх" и "вниз", "влево" и "вправо" для правки, TAB для автодополнения). Вы можете запустить следующую команду и будете готовы к работе:

  • zef install Linenoise для Windows, Linux и macOS

  • zef install Readline, если вы пользуетесь Linux и предпочитаете библиотеку Readline

1.5. Текстовые редакторы

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

Лично я пользуюсь и рекомендую Atom. Это современный текстовый редактор, способный "из коробки" подсвечивать синтаксис Raku. Perl 6 FE - альтернативный плагин подсветки кода в Atom, "отколовшийся" от оригинального плагина, содержащий много исправлений и улучшений.

Други люди из нашего сообщества также пользуются Vim, GNU Emacs или Padre.

Последние версии Vim’a имеют подсветку синтаксиса "из коробки". Emacs и Padre потребуют установки дополнительных плагинов.

1.6. Привет, Мир!

Нам стоит начать с ритуала hello world.

say 'hello world';

ещё это можно написать так:

'hello world'.say;

1.7. Обзор синтаксиса

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

Инструкции обычно являются логическими строками кода, они должны заканчиваться точкой с запятой: say "Hello" if True;

Выражения являются особым типом инструкций, возвращающим значение: 1+2 вернёт 3

Выражения состоят из Термов и Операторов.

Термами являются:

  • Переменные: значения, которые можно изменять.

  • Литералы: константные значения вроде чисел или строк.

Классификация операторов:

Тип

Описание

Пример

Префиксный

Перед термом

++1

Инфиксный

Между термами

1+2

Постфиксный

После терма

1++

Циркумфиксный

Вокруг терма

(1)

Постциркумфиксный

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

Array[1]

1.7.1. Идентификаторы

Идентификаторы это имена, которые даются термам при определении.

Правила:
  • Они должны начинаться с алфавитной буквы или с нижнего подчеркивания.

  • Они могут содержать цифры (за исключением первого символа).

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

Правильно

Неправильно

var1

1var

var-one

var-1

var-one

var'1

var1_

var1'

_var

-var

Соглашения по именованию:
  • Camel case: variableNo1

  • Kebab case: variable-no1

  • Snake case: variable_no1

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

Использование осмысленных имён упростит вашу (и остальных) жизнь программиста.

  • var1 = var2 * var3 синтаксически правильно, но смысл не очевиден.

  • monthly-salary = daily-rate * working-days будет лучшим вариантом названий переменных.

1.7.2. Комментарии

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

Комментарии делятся на три типа:

  • Однострочный:

    # Это однострочный комментарий
  • Встроенный:

    say #`(Это встроенный комментарий) "Hello World."
  • Многострочный:

    =begin comment
    Это многострочный комментарий.
    Комментарий 1
    Комментарий 2
    =end comment

1.7.3. Кавычки

Строки должны быть ограничены либо двойными, либо одиночными кавычками.

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

  • если строка содержит апостроф

  • если строка содержит интерполируемую переменную.

say 'Hello World';    # Hello World
say "Hello World";    # Hello World
say "Don't";          # Don't
my $name = 'John Doe';
say 'Hello $name';    # Hello $name
say "Hello $name";    # Hello John Doe

2. Операторы

2.1. Общие операторы

В таблице ниже приведён список наиболее распространённых операторов.

Оператор Тип Описание Пример Результат

+

Инфиксный

Сложение

1 + 2

3

-

Инфиксный

Вычитание

3 - 1

2

*

Инфиксный

Умножение

3 * 2

6

**

Инфиксный

Возведение в степень

3 ** 2

9

/

Инфиксный

Деление

3 / 2

1.5

div

Инфиксный

Целочисленное деление (округление вниз)

3 div 2

1

%

Инфиксный

Остаток деления

7 % 4

3

%%

Инфиксный

Проверка делимости

6 %% 4

False

6 %% 3

True

gcd

Инфиксный

Наибольший общий делитель

6 gcd 9

3

lcm

Инфиксный

Наименьшее общее кратное

6 lcm 9

18

==

Инфиксный

Числовое равенство

9 == 7

False

!=

Инфиксный

Числовое неравенство

9!= 7

True

<

Инфиксный

Меньше, чем

9 < 7

False

>

Инфиксный

Больше, чем

9 > 7

True

<=

Инфиксный

Меньше либо равно

7 <= 7

True

>=

Инфиксный

Больше либо равно

9 >= 7

True

<=>

Инфиксный

Трёхстороннее сравнение

1 <=> 1.0

Same

1 <=> 2

Less

3 <=> 2

More

eq

Инфиксный

Равенство строк

"John" eq "John"

True

ne

Инфиксный

Неравенство строк

"John" ne "Jane"

True

lt

Инфиксный

Строка меньше, чем

"a" lt "b"

True

gt

Инфиксный

Строка больше, чем

"a" gt "b"

False

le

Инфиксный

Строка меньше либо равна

"a" le "a"

True

ge

Инфиксный

Строка больше либо равна

"a" ge" "b"

False

leg

Инфиксный

Трёхстороннее сравнение строк

"a" leg "a"

Same

"a" leg "b"

Less

"c" leg "b"

More

cmp

Инфиксный

Умное трёхстороннее сравнение

"a" cmp "b"

Less

3.5 cmp 2.6

More

=

Инфиксный

Присвоение

my $var = 7

Присваивает переменной $var значение 7

~

Инфиксный

Конкатенация строк

9 ~ 7

97

"Hi " ~ "there"

Hi there

x

Инфиксный

Умножение строк

13 x 3

131313

"Hello " x 3

Hello Hello Hello

~~

Инфиксный

Соответствие

2 ~~ 2

True

2 ~~ Int

True

"Perl 6" ~~ "Perl 6"

True

"Perl 6" ~~ Str

True

"enlightenment" ~~ /light/

「light」

++

Префиксный

Инкремент

my $var = 2; ++$var;

Прибавляет к значению переменной 1 и возвращает результат 3

Постфиксный

Инкремент

my $var = 2; $var++;

Возвращает переменную 2 и приращивает его значение

--

Префиксный

Декремент

my $var = 2; --$var;

Уменьшает переменную на 1 и возвращает результат 1

Постфиксный

Декремент

my $var = 2; $var--;

Возвращает переменную 2 и уменьшает его значение

+

Префиксный

Приведение операнда к числовому значению

+"3"

3

+True

1

+False

0

-

Префиксный

Приведение операнда к числовому значению и отрицание

-"3"

-3

-True

-1

-False

0

?

Префиксный

Приведение операнда к логическому значению

?0

False

?9.8

True

?"Hello"

True

?""

False

my $var; ?$var;

False

my $var = 7; ?$var;

True

!

Префиксный

Приведение операнда к логическому значению и отрицание

!4

False

..

Инфиксный

Конструктор диапазона

0..5

Создаёт диапазон интервала [0, 5] [1]

..^

Инфиксный

Конструктор диапазона

0..^5

Создаёт диапазон интервала [0, 5) [1]

^..

Инфиксный

Конструктор диапазона

0^..5

Создаёт диапазон интервала (0, 5] [1]

^..^

Инфиксный

Конструктор диапазона

0^..^5

Создаёт диапазон интервала (0, 5) [1]

^

Префиксный

Конструктор диапазона

^5

Как и 0..^5, создаёт диапазон интервала [0, 5) [1]

…​

Инфиксный

Конструктор "ленивого" списка

0…​9999

Возвращкет только затребованные элементы

|

Префиксный

Выравнивание

|(0..5)

(0 1 2 3 4 5)

|(0^..^5)

(1 2 3 4)

2.2. Обратные операторы

Добавление R перед любым из операторов меняет местами операнды.

Обычная операция Результат Обратный оператор Результат

2 / 3

0.666667

2 R/ 3

1.5

2 - 1

1

2 R- 1

-1

2.3. Операторы свёртки

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

Обычная операция Результат Оператор свёртки Результат

1 + 2 + 3 + 4 + 5

15

[+] 1,2,3,4,5

15

1 * 2 * 3 * 4 * 5

120

[*] 1,2,3,4,5

120

С более полным списком операторов и их старшинством вы можете ознакомиться на https://docs.raku.org/language/operators

3. Переменные

Переменные Raku разделены на три категории: Скаляры, Массивы и Хеши.

Сигил ("печать" на латыни) - это символ, который используется в качестве префикса, категоризирующего переменные.

  • $ используется со скалярами

  • @ используется с массивами

  • % используется с хешами

3.1. Скаляры

Скаляр содержит одно значение или ссылку.

# Строка
my $name = 'John Doe';
say $name;

# Целое число
my $age = 99;
say $age;

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

Строка
my $name = 'John Doe';
say $name.uc;
say $name.chars;
say $name.flip;
JOHN DOE
8
eoD nhoJ
С полным списком методов, применяемых к строкам, можно ознакомиться на https://docs.raku.org/type/Str
Целое число
my $age = 17;
say $age.is-prime;
True
С полным списком методов, применяемых к целым числам, можно ознакомиться на https://docs.raku.org/type/Int
Рациональное число
my $age = 2.3;
say $age.numerator;
say $age.denominator;
say $age.nude;
23
10
(23 10)
С полным списком методов, применяемых к рациональным числам, можно ознакомиться на https://docs.raku.org/type/Rat

3.2. Массивы

Массивы являются списками, содержащими множество значений.

my @animals = 'camel','llama','owl';
say @animals;

Как показано на примере ниже, есть множество встроенных операций над массивами:

Тильда ~ используется для конкатенации строк.
Скрипт
my @animals = 'camel','vicuña','llama';
say "The zoo contains " ~ @animals.elems ~ " animals";
say "The animals are: " ~ @animals;
say "I will adopt an owl for the zoo";
@animals.push("owl");
say "Now my zoo has: " ~ @animals;
say "The first animal we adopted was the " ~ @animals[0];
@animals.pop;
say "Unfortunately the owl got away and we're left with: " ~ @animals;
say "We're closing the zoo and keeping one animal only";
say "We're going to let go: " ~ @animals.splice(1,2) ~ " and keep the " ~ @animals;
Вывод
The zoo contains 3 animals
The animals are: camel vicuña llama
I will adopt an owl for the zoo
Now my zoo has: camel vicuña llama owl
The first animal we adopted was the camel
Unfortunately the owl got away and we're left with: camel vicuña llama
We're closing the zoo and keeping one animal only
We're going to let go: vicuña llama and keep the camel
Пояснение

.elems возвращает количество элементов в массиве.
.push() добавляет один или более элементов в массив.
Мы можем получить доступ к конкретному элементу массива через указание его индекса @animals[0].
.pop удаляет последний элемент массива и возвращает его
.splice(a,b) удалит b элементов, начиная с индекса a.

3.2.1. Массивы с фиксированным размером

Обычный массив объявляется так:

my @array;

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

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

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

my @array[3];

Этот массив будет хранить не более 3 значений, с индексами от 0 до 2.

my @array[3];
@array[0] = "first value";
@array[1] = "second value";
@array[2] = "third value";

Вы не сможете добавить в этот массив четвёртый элемент:

my @array[3];
@array[0] = "first value";
@array[1] = "second value";
@array[2] = "third value";
@array[3] = "fourth value";
Index 3 for dimension 1 out of range (must be 0..2)

3.2.2. Многомерный массив

Массивы, которые мы видели до сих пор, были одномерными.
К счастью, мы можем работать с многомерными массивами в Raku.

my @tbl[3;2];

Этот массив является двумерным. Первое измерение может содержать максимум три значения, а второе - только две.

Представляйте сетку значений 3x2 .

my @tbl[3;2];
@tbl[0;0] = 1;
@tbl[0;1] = "x";
@tbl[1;0] = 2;
@tbl[1;1] = "y";
@tbl[2;0] = 3;
@tbl[2;1] = "z";
say @tbl
[[1 x] [2 y] [3 z]]
Визуальное отображение массива:
[1 x]
[2 y]
[3 z]
Более подробно с типом Array можно ознакомиться на https://docs.raku.org/type/Array

3.3. Хеши

Хеш - это набор пар ключ-значение.
my %capitals = ('UK','London','Germany','Berlin');
say %capitals;
Ещё один простой способ заполнить хеш:
my %capitals = (UK => 'London', Germany => 'Berlin');
say %capitals;

Вот несколько методов, определённых для хешей:

Скрипт
my %capitals = (UK => 'London', Germany => 'Berlin');
%capitals.push: (France => 'Paris');
say %capitals.kv;
say %capitals.keys;
say %capitals.values;
say "The capital of France is: " ~ %capitals<France>;
Вывод
(France Paris Germany Berlin UK London)
(France Germany UK)
(Paris Berlin London)
The capital of France is: Paris
Пояснение

.push: (key => 'Value') добавляет новую пару ключ-значение.
.kv возвращает список, содержащий все ключи и значения.
.keys возвращает список, содержащий все ключи.
.values возвращает список, содержащий все значения.
Мы можем получить доступ к отдельному значению в хеше, указав его ключ %hash<key>

Более подробно с типом Hash можно ознакомиться на https://docs.raku.org/type/Hash

3.4. Типы

В прошлых примерах мы не определяли тип значения для переменных.

.WHAT вернёт тип значения переменной.
my $var = 'Text';
say $var;
say $var.WHAT;

$var = 123;
say $var;
say $var.WHAT;

Как вы можете видеть на примере выше, тип значения $var был сначала (Str), а потом стал (Int).

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

А сейчас попробуйте запустить следующий пример:
Обратите внимание на Int перед именем переменной.

my Int $var = 'Text';
say $var;
say $var.WHAT;

Этот код завершится исключением с таким сообщением: Type check failed in assignment to $var; expected Int but got Str.

Произошло вот что - мы заранее определили, что переменная должна быть типа (Int). Когда же мы попытались присвоить ей (Str), было сгенерировано исключение и выполнение программы прекратилось.

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

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

Массивы и хеши тоже могут быть статически типизированы:
my Int @array = 1,2,3;
say @array;
say @array.WHAT;

my Str @multilingual = "Hello","Salut","Hallo","您好","안녕하세요","こんにちは";
say @multilingual;
say @multilingual.WHAT;

my Str %capitals = (UK => 'London', Germany => 'Berlin');
say %capitals;
say %capitals.WHAT;

my Int %country-codes = (UK => 44, Germany => 49);
say %country-codes;
say %country-codes.WHAT;
Ниже - список наиболее часто используемых типов:

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

Тип

Описание

Пример

Результат

Mu

Базовый тип иерархии типов в Raku

Any

Базовый класс по умолчанию для новых классов и для большинства встроенных классов

Cool

Значение, которое можно взаимозаменяемо использовать и как строку и как число

my Cool $var = 31; say $var.flip; say $var * 2;

13 62

Str

Строка символов

my Str $var = "NEON"; say $var.flip;

NOEN

Int

Целое число (произвольной точности)

7 + 7

14

Rat

Рациональное число (ограниченной точности)

0.1 + 0.2

0.3

Bool

Булево значение

!True

False

3.5. Интроспекция

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

my Int $var;
say $var.WHAT;    # (Int)
my $var2;
say $var2.WHAT;   # (Any)
$var2 = 1;
say $var2.WHAT;   # (Int)
$var2 = "Hello";
say $var2.WHAT;   # (Str)
$var2 = True;
say $var2.WHAT;   # (Bool)
$var2 = Nil;
say $var2.WHAT;   # (Any)

Тип переменной, содержащей значение, зависит от этого значения.
Тип строго определённой пустой переменной будет тем типом, который был объявлен.
Пустая переменная без объявленного типа будет иметь тип Any.
Чтобы обнулить значение переменной, присвойте ей Nil.

3.6. Область видимости

Прежде чем использовать переменную впервые, она должен быть объявлена.

В Raku используют несколько вариантов объявления области видимости. Пока что мы использовали только my.

my $var=1;

Объявление области видимости my присваивает переменной лексическую область видимости. Иными словами, переменная будет доступна только в пределах блока, в котором она была объявлена.

Блок в Raku обозначается { }. Переменные, объявленные за пределами блока, будут доступны во всем скрипте.

{
  my Str $var = 'Text';
  say $var;   # достижима
}
say $var;   # не достижима, вернёт ошибку

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

{
  my Str $var = 'Text';
  say $var;
}
my Int $var = 123;
say $var;

3.7. Присваивание vs. Привязывание

В предыдущем примере мы видели, как присвоить значение к переменной.
Присвоение совершается используя оператор =.

my Int $var = 123;
say $var;

Мы можем изменить значение, присвоенное переменной:

Присваивание
my Int $var = 123;
say $var;
$var = 999;
say $var;
Вывод
123
999

С другой стороны, мы не можем изменить значение, которое было привязано к переменной.
Привязка (binding) совершается с использованием оператора :=.

Привязка
my Int $var := 123;
say $var;
$var = 999;
say $var;
Вывод
123
Cannot assign to an immutable value
Переменные также можно привязывать к другим переменным:
my $a;
my $b;
$b := $a;
$a = 7;
say $b;
$b = 8;
say $a;
Вывод
7
8

Привязка переменных является двунаправленной.
$a := $b и $b := $a имеют один и тот же результат.

Больше о переменных можно узнать на https://docs.perl6.org/language/variables

4. Функции и мутаторы

Важно различать функции и мутаторы.
Функции не меняют состояние объекта, к которому они применяются.
Мутаторы же вносят изменения в состояние объекта.

Скрипт
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
my @numbers = [7,2,4,9,11,3];

@numbers.push(99);
say @numbers;      #1

say @numbers.sort; #2
say @numbers;      #3

@numbers.=sort;
say @numbers;      #4
Вывод
[7 2 4 9 11 3 99] #1
(2 3 4 7 9 11 99) #2
[7 2 4 9 11 3 99] #3
[2 3 4 7 9 11 99] #4
Пояснение

.push - мутатор; он изменяет состояние массива (#1)

.sort - функция; она возвращает отсортированный массив, но не изменяет состояние изначального массива:

  • (#2) показывает, что она вернула отсортированный массив.

  • (#3) показывает, что начальный массив не изменился.

Чтобы заставить функцию работать в качестве мутатора, можно использовать .= вместо . (#4) (строка 9 в скрипте)

5. Циклы и условия

В Raku есть много способов описать условие и цикл.

5.1. if

Код выполняется только при соблюдённом условии; то есть вычисленное выражение равно True.

my $age = 19;

if $age > 18 {
  say 'Welcome'
}

В Raku вы можем поменять местами код и условие.
Даже если условие определено после кода, оно всегда будет проверяться в первую очередь.

my $age = 19;

say 'Welcome' if $age > 18;

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

  • else

  • elsif

# запускаем тот же самый код с разными значениями переменной
my $number-of-seats = 9;

if $number-of-seats <= 5 {
  say 'I am a sedan'
} elsif $number-of-seats <= 7 {
  say 'I am 7 seater'
} else {
  say 'I am a van'
}

5.2. unless

Противоположность оператора if - unless.

Данный код:

my $clean-shoes = False;

if not $clean-shoes {
  say 'Clean your shoes'
}

может быть записан так:

my $clean-shoes = False;

unless $clean-shoes {
  say 'Clean your shoes'
}

Логическое отрицание в Raku осуществляется с помощью ! или not.

unless (условие) обычно используется вместо if not (условие).

unless не может иметь ветку else .

5.3. with

with действует как и инструкция if, но проверяет, определена ли переменная.

my Int $var=1;

with $var {
  say 'Hello'
}

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

my Int $var;

with $var {
  say 'Hello'
}

without является противоположностью with. Эту конструкцию можно сравнить с unless.

Если первое условие with не соблюдено, можно описать альтернативную ветка используя orwith.
with и orwith можно сравнить с if и elsif.

5.4. for

Цикл for обходит множество значений.

my @array = [1,2,3];

for @array -> $array-item {
  say $array-item * 100
}

Обратите внимание, что мы создали переменную цикла $array-item, а затем осуществили операцию *100 над каждым из элементов массива.

5.5. given

given - эквивалент конструкции "switch" в других языках, но намного круче.

my $var = 42;

given $var {
    when 0..50 { say 'Less than or equal to 50'}
    when Int { say "is an Int" }
    when 42  { say 42 }
    default  { say "huh?" }
}

После первого успешного сравнения, процесс выбора остановится.

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

my $var = 42;

given $var {
    when 0..50 { say 'Less than or equal to 50';proceed}
    when Int { say "is an Int";proceed}
    when 42  { say 42 }
    default  { say "huh?" }
}

5.6. loop

loop - ещё один способ написать цикл for.

На самом деле, loop - это то, как циклы for записываются в семействе языков программирования C.

Raku принадлежит к этому семейству.

loop (my $i = 0; $i < 5; $i++) {
  say "The current number is $i"
}
Больше о циклах и условиях можно узнать на https://docs.raku.org/language/control

6. Ввод-вывод

В Raku, два наиболее распространённых интерфейса ввода-вывода это Эмулятор терминала и Файлы.

6.1. Базовый ввод-вывод в коммандной строке

6.1.1. say

say делает вывод в стандартный поток вывода. Он добавляет символ перевода строки в конце. Иначе говоря, этот код:

say 'Hello Mam.';
say 'Hello Sir.';

будет выведен как две раздельные строки.

6.1.2. print

print, с другой стороны, действует как say, но не добавляет символ новой строки.

Попробуйте заменить say на print и сравните результаты.

6.1.3. get

get используется для получения ввода из командной строки.

my $name;

say "Hi, what's your name?";
$name = get;

say "Dear $name welcome to Raku";

После запуска кода выше, строка будет ожидать ввода имени. Введите его и нажмите [Enter]. Далее, код поприветствует вас.

6.1.4. prompt

prompt - это комбинация print и get.

Пример выше может быть записан так:

my $name = prompt "Hi, what's your name? ";

say "Dear $name welcome to Raku";

6.2. Запуск shell-команд

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

  • run запускает внешнюю команду без вызова командной оболочки

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

Запустите это, если вы пользуетесь Linux/macOS
my $name = 'Neo';
run 'echo', "hello $name";
shell "ls";
Запустите это, если вы пользуетесь Windows
shell "dir";

echo и ls - распространённые команды оболочки в Linux:
echo выводит текст в эмулятор терминала (эквивалент print в Perl 6)
ls выводит список всех файлов и папок в текущей директории

dir эквивалент ls в Windows.

6.3. Файловый ввод-вывод

6.3.1. slurp

slurp используется для чтения данных из файла.

Создайте текстовый файл со следующим содержанием:

datafile.txt
John 9
Johnnie 7
Jane 8
Joanna 7
my $data = slurp "datafile.txt";
say $data;

6.3.2. spurt

spurt используется для записи данных в файл.

my $newdata = "New scores:
Paul 10
Paulie 9
Paulo 11";

spurt "newdatafile.txt", $newdata;

После запуска кода выше, будет создан новый файл newdatafile.txt. Он будет содержать новые оценки.

6.4. Работа с файлами и директориями

В Raku можно получить список содержимого директории, не прибегая к командам оболочки вроде ls.

say dir;                # Перечисляет файлы и папки в текущей директории
say dir "/Documents";   # Перечисляет файлы и папки в указанной директории

Также вы можете создавать и удалять директории.

mkdir "newfolder";
rmdir "newfolder";

mkdir создаёт новую директорию.
rmdir удаляет пустую директорию или возвращает ошибку, если она не пуста.

Вы также можете проверить, существует ли путь, является ли он файлом или директорией:

В директории, в которой вы запустите приведённый ниже скрипт, создайте пустую папку folder123 и пустой файл script123.p6

say "script123.p6".IO.e;
say "folder123".IO.e;

say "script123.p6".IO.d;
say "folder123".IO.d;

say "script123.p6".IO.f;
say "folder123".IO.f;

IO.e проверяет, существует ли директория/файл.
IO.f проверяет, ведёт ли путь к файлу.
IO.d проверяет, ведёт ли путь к директории.

ВНИМАНИЕ: Пользователи Windows могут использовать / или \\, чтобы разделять директории
C:\\rakudo\\bin
C:/rakudo/bin

Больше информации о вводе/выводе можно узнать на https://docs.raku.org/type/IO

7. Подпрограммы

7.1. Определение

Подпрограммы (также называемые функциями) это средство объединения и повторного использования функциональности.

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

sub alien-greeting {
  say "Hello earthlings";
}

alien-greeting;

Это пример определения подпрограммы, в которой нет входных данных.

7.2. Сигнатура

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

Подпрограмма ниже принимает аргументом строку.

sub say-hello (Str $name) {
    say "Hello " ~ $name ~ "!!!!"
}
say-hello "Paul";
say-hello "Paula";

7.3. Множественная диспетчеризация

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

multi greet($name) {
    say "Good morning $name";
}
multi greet($name, $title) {
    say "Good morning $title $name";
}

greet "Johnnie";
greet "Laura","Mrs.";

7.4. Параметры по умолчанию и опциональные параметры

Если подпрограмма принимает аргумент, но вызвана без этого необходимого аргумента, произойдёт ошибка.

Raku предоставляет нам возможность определять подпрограммы с:

  • Опциональными параметрами

  • Параметрами по умолчанию

Опциональный параметр можно определить, добавив ? к его имени.

sub say-hello($name?) {
  with $name { say "Hello " ~ $name }
  else { say "Hello Human" }
}
say-hello;
say-hello("Laura");

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

sub say-hello($name="Matt") {
  say "Hello " ~ $name;
}
say-hello;
say-hello("Laura");

7.5. Возврат значений

Все подпрограммы, которые мы пока видели, делали что-то — они выводили некий текст в терминал.

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

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

Неявный возврат
sub squared ($x) {
  $x ** 2;
}
say "7 squared is equal to " ~ squared(7);

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

Явный возврат
sub squared ($x) {
  return $x ** 2;
}
say "7 squared is equal to " ~ squared(7);

7.5.1. Указание типа возвращаемого значения

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

Для того, чтобы ограничить возвращаемое значение определённым типом, мы используем либо трейт returns либо стрелочную нотацию --> в сигнатуре.

Использование трейта returns
sub squared ($x) returns Int {
  return $x ** 2;
}
say "1.2 squared is equal to " ~ squared(1.2);
Использование стрелки
sub squared ($x --> Int) {
  return $x ** 2;
}
say "1.2 squared is equal to " ~ squared(1.2);

Если мы вернём значение, которое не будет совпадать с указанным типом, будет выброшена ошибка.

Type check failed for return value; expected Int but got Rat (1.44)

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

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

Мы также могли указать, что возвращаемый тип Int должен быть строго определённым либо строго неопределённым, используя такие сигнатуры: :
-→ Int:D и -→ Int:U

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

sub squared ($x --> Int:D) {
  return $x ** 2;
}
say "1.2 squared is equal to " ~ squared(1.2);
Более подробно о подпрограммах и функциях можно узнать на https://docs.raku.org/language/functions

8. Функциональное программирование

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

8.1. Функции это объекты первого класса

Функции/подпрограммы это объекты первого класса:

  • Они могут быть переданы как аргументы

  • Они могут быть возвращены из других функций

  • Они могут быть присвоены переменным

Наглядным примером является функция map.
map это функция высшего порядка, она может принимать другую функцию как аргумент.

Скрипт
my @array = <1 2 3 4 5>;
sub squared($x) {
  $x ** 2
}
say map(&squared,@array);
Вывод
(1 4 9 16 25)
Пояснение

Мы определили подпрограмму с именем squared, которая принимает аргумент и умножает его на себя.
Далее, мы использовали map, функцию высшего порядка, и передали ей два аргумента: подпрограмма squared и массив.
Результат - список элементов массива, возведённых в квадрат.

Отметьте, что при передаче подпрограммы аргументом, мы должны добавлять & в начале её имени.

8.2. Анонимные функции

Анонимные функции также называют лямбдами.
Анонимная функция не привязана к идентификатору (у неё нет имени).

Давайте перепишем пример с map, используя анонимную функцию

my @array = <1 2 3 4 5>;
say map(-> $x {$x ** 2},@array);

Отметьте, что вместо объявления подпрограммы для получения квадрата и передачи её аргументом к map, мы определили её внутри анонимной подпрограммы как -> $x {$x ** 2}.

В жаргоне Perl 6, мы называем такую нотацию заостренный блок (pointy block)

Заостренный блок также можно использовать, чтобы присвоить функцию переменной:
my $squared = -> $x {
  $x ** 2
}
say $squared(9);

8.3. "Сцепление"

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

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

Вот решение без сцепления:

my @array = <7 8 9 0 1 2 4 3 5 6 7 8 9>;
my @final-array = reverse(sort(unique(@array)));
say @final-array;

В этом случае, мы вызываем unique на @array, передаём результат как аргумент в sort, а затем передаём результат в reverse.

Напротив, с сцеплением методов, пример выше может быть переписан как:

my @array = <7 8 9 0 1 2 4 3 5 6 7 8 9>;
my @final-array = @array.unique.sort.reverse;
say @final-array;

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

8.4. Оператор ленты

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

Прямая лента
my @array = <7 8 9 0 1 2 4 3 5 6 7 8 9>;
@array ==> unique()
       ==> sort()
       ==> reverse()
       ==> my @final-array;
say @final-array;
Пояснение
Начать с `@array` и вернуть список уникальных элементов
                  и отсортировать его
                  и развернуть его
                  и сохранить результат в @final-array

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

Обратная лента
my @array = <7 8 9 0 1 2 4 3 5 6 7 8 9>;
my @final-array-v2 <== reverse()
                   <== sort()
                   <== unique()
                   <== @array;
say @final-array-v2;
Пояснение

Обратная лента работает как прямая, но в противоположную сторону.
Порядок вызовов методов снизу-вверх — от последнего до первого шага.

8.5. Гипер оператор

Гипер оператор >>. вызовет метод для каждого из элементов списка и вернёт список результатов.

my @array = <0 1 2 3 4 5 6 7 8 9 10>;
sub is-even($var) { $var %% 2 };

say @array>>.is-prime;
say @array>>.&is-even;

Используя гипер оператор мы можем вызывать методы, уже определённые в Raku, например, is-prime, который определяет является ли число простым или нет.
Вдобавок мы можем определять новые подпрограммы и вызывать их, используя этот оператор. В этом случае нам необходимо добавлять & перед именем метода, например, &is-even.

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

Perl 6 гарантирует, что порядок результатов будет таким же, как и в оригинальном списке. Однако, нет гарантии, что Perl 6 на самом деле вызовет методы в оригинальном порядке или в том же самом потоке. Поэтому, будьте осторожны с методами, у которых есть побочные эффекты, например say или print.

8.6. Скрещение

Скрещение это логическая суперпозиция значений.

В примере ниже, 1|2|3 это скрещение.

my $var = 2;
if $var == 1|2|3 {
  say "The variable is 1 or 2 or 3"
}

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

8.7. Ленивые Списки

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

Преимущества такого подхода:

  • Увеличение производительности благодаря избеганию ненужных вычислений

  • Возможность создавать потенциально бесконечные структуры данных

  • Возможность определять поток выполнения

Для создания ленивого списка, мы используем инфиксный оператор …​
Ленивый список имеет начальные элементы (один либо больше), генератор и предел.

Простой ленивый список
my $lazylist = (1 ... 10);
say $lazylist;

Начальный элемент - 1, а предел - 10. Генератор не был определён, поэтому используется генератор по умолчанию - следующий элемент (+1)
Иными словами, ленивый список может вернуть (если необходимо) следующие элементы: (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

Бесконечный ленивый список
my $lazylist = (1 ... Inf);
say $lazylist;

Этот список вернёт, если необходимо, любое целое число от 1 до бесконечности, то есть любое целое число.

Ленивый список с использованием выведенного генератора
my $lazylist = (0,2 ... 10);
say $lazylist;

Начальные элементы здесь 0 и 2, а предел - 10. Мы не определили генератор, но используя начальные значения, Raku "выведет", что генератор это (+2)
Такой список вернёт, если необходимо, следующие элементы: (0, 2, 4, 6, 8, 10)

Ленивый список с определённым генератором
my $lazylist = (0, { $_ + 3 } ... 12);
say $lazylist;

В этом примере, мы явно определили генератор, заключённый в скобки { }
Этот ленивый список может вернуть (если необходимо) следующие элементы: (0, 3, 6, 9, 12)

При использовании явного генератора, предел должен быть одним из значений, которые этот генератор может вернуть.
Если мы запустим предыдущий пример с пределом 10 вместо 12, он не остановится. Генератор перепрыгнет через предел.

В качестве альтернативы, мы можем заменить 0 …​ 10 на 0 …​^ * > 10
Такое выражение читается как "От 0 до первого значения больше, чем 10 (исключая его)"

Такой генератор не остановится
my $lazylist = (0, { $_ + 3 } ... 10);
say $lazylist;
Такой генератор остановится
my $lazylist = (0, { $_ + 3 } ...^ * > 10);
say $lazylist;

8.8. Замыкания

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

sub generate-greeting {
    my $name = "John Doe";
    sub greeting {
      say "Good Morning $name";
    };
    return &greeting;
}
my $generated = generate-greeting;
$generated();

Если вы запустите код выше, он выведет Good Morning John Doe в эмуляторе терминала.
Хотя результат очень простой, этот пример интересен тем, что внутренняя подпрограмма greeting была возвращена из внешней до своего вызова.

$generated стала замыканием.

Замыкание это особый вид объекта, который содержит две вещи:

  • Подпрограмма

  • Окружение, в котором эта подпрограмма была создана.

Окружение состоит из всех локальных переменных, которые были в области видимости во время создания замыкания. В этом случае, $generated это замыкание, которое объединяет подпрограмму greeting и строку John Doe, которая существовала, когда замыкание было создано.

Давайте взглянем на более интересный пример.

sub greeting-generator($period) {
  return sub ($name) {
    return "Good $period $name"
  }
}
my $morning = greeting-generator("Morning");
my $evening = greeting-generator("Evening");

say $morning("John");
say $evening("Jane");

В этом примере, мы определили подпрограмму greeting-generator($period), которая принимает новую подпрограмму. Подпрограмма, которую она возвращает, принимает единственный аргумент $name и возвращает созданное приветствие.

По сути, greeting-generator это фабрика подпрограмм. В этом примере, мы использовали greeting-generator, чтобы создать две подпрограммы, одна из которых выводит Good Morning, а другая - Good Evening.

$morning и $evening это замыкания. Они разделяют одинаковое тело подпрограммы, но разные окружения.
В окружении $morning, $period это Morning. В окружении $evening, $period это Evening.

9. Классы и Объекты

В предыдущем разделе, мы изучили возможности Raku в функциональном программировании.
В этом разделе мы посмотрим на объектно-ориентированное программирование в Raku.

9.1. Введение

Объектно-ориентированное программирование это одна из широко используемых сегодня парадигм программирования.
Объект - это набор переменных и подпрограмм, объединенный в одну сущность.
Переменные называются атрибутами, а подпрограммы - методами.
Атрибуты определяют состояние, а методы определяют поведение объекта.

Класс это шаблон для создания объектов.

Для того, чтобы понять это отношение, рассмотрим следующий пример:

Есть 4 человека в комнате

объекты ⇒ 4 человека

Эти 4 человека - люди

класс ⇒ человек

У них разные имена, возраст, пол и национальность

атрибуты ⇒ имя, возраст, пол, национальность

В объектно-ориентированном жаргоне, мы называем объекты экземплярами класса.

Рассмотрим следующий скрипт:

class Human {
  has $.name;
  has $.age;
  has $.sex;
  has $.nationality;
}

my $john = Human.new(name => 'John', age => 23, sex => 'M', nationality => 'American');
say $john;

Ключевое слово class используется для определения класса.
Ключевое слово has используется для определения атрибутов класса.
Метод .new() вызывает конструктор. Он создаёт объект как экземпляр класса, для которого он был вызван.

В скрипте выше, новая переменная $john содержит ссылку на новый экземпляр класса "Human", определённую вызовом Human.new().
Аргументы переданные в метод .new() используются для установки значений атрибутов созданного объекта.

Классу можно присвоить лексическую область видимости используя my:

my class Human {

}

9.2. Инкапсуляция

Инкапсуляция это концепция объектно-ориентированного программирования, которая состоит в том, чтобы объединять наборы данных и методов вместе.
Данные (атрибуты) внутри объекта должны быть приватными, иными словами, доступными только лишь изнутри этого объекта.
Для того, чтобы обратиться к атрибутам не в теле метода объекта, мы используем методы, называемые ацессоры(методы доступа).

Два скрипта ниже имеют одинаковый результат.

Явный доступ к переменной:
my $var = 7;
say $var;
Инкапсуляция:
my $var = 7;
sub sayvar {
  $var;
}
say sayvar;

Метод sayvar это метод доступа. Он позволяет нам получать доступ к значению переменной без прямого обращения к ней.

Инкапсуляция в Raku упрощается с использованием твигилов.
Твигилы это вторичные сигилы. Они находятся между сигилом и именем атрибута.
В классах используются два твигила:

  • ! используется для явного объявления атрибута как приватного.

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

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

Таким образом, мы можем переписать класс выше как:

class Human {
  has $!name;
  has $!age;
  has $!sex;
  has $!nationality;
}

my $john = Human.new(name => 'John', age => 23, sex => 'M', nationality => 'American');
say $john;

Добавьте в скрипт следующую инструкцию: say $john.age;
Повторный запуск скрипта выведет такую ошибку: Method 'age' not found for invocant of class 'Human' потому что $!age приватный и может быть использован лишь внутри объекта. Попытка обратиться к нему снаружи объекта создаст исключение.

Теперь замените has $!age на has $.age и проверьте результат инструкции say $john.age;

9.3. Именованные и Позиционные параметры

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

  • name => 'John'

  • age => 23

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

class Human {
  has $.name;
  has $.age;
  has $.sex;
  has $.nationality;
  # новый конструктор, который переопределяет конструктор по умолчанию
  method new ($name,$age,$sex,$nationality) {
    self.bless(:$name,:$age,:$sex,:$nationality);
  }
}

my $john = Human.new('John',23,'M','American');
say $john;

9.4. Методы

9.4.1. Введение

Методы это подпрограммы объекта.
Как и подпрограммы, это средство объединения и повторного использования функциональности, они принимают аргументы, имеют сигнатуру и могут быть определены как multi.

Методы определяются с помощью ключевого слова method.
Обычно методы выполняют некие действия над атрибутами объекта. Это реализует концепцию инкапсуляции. Атрибуты объекта могут быть использованы только изнутри объекта, используя методы. Внешний код может взаимодействовать с методами объекта, но не имеет прямого доступа к его атрибутам.

class Human {
  has $.name;
  has $.age;
  has $.sex;
  has $.nationality;
  has $.eligible;
  method assess-eligibility {
      if self.age < 21 {
        $!eligible = 'No'
      } else {
        $!eligible = 'Yes'
      }
  }

}

my $john = Human.new(name => 'John', age => 23, sex => 'M', nationality => 'American');
$john.assess-eligibility;
say $john.eligible;

Методы, однажды определённые для класса, могут быть вызваны для объекта используя точечную нотацию:
объект . метод либо как в предыдущем примере: $john.assess-eligibility

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

Внутри определения метода, если нам необходимо обратиться к атрибуту, мы используем сигил !, даже если он был определён через .
Смысл этого в том, что всё, что делает твигил ., это объявляет атрибут с ! и автоматически создаёт ацессор.

В примере выше, if self.age < 21 и if $!age < 21 будут иметь одинаковый эффект, хотя технически они различаются:

  • self.age вызывает метод .age (ацессор)
    Может быть иначе записано как $.age

  • $!age это прямое обращение к переменной

9.4.2. Приватные методы

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

Приватные методы - это методы, которые могут быть вызваны лишь в пределах объявления класса.

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

Для объявления приватного метода необходимо использовать твигил ! перед его именем.
Приватные методы вызываются с помощью ! вместо .

method !iamprivate {
  # код здесь
}

method iampublic {
  self!iamprivate;
  # дополнительные действия
}

9.5. Атрибуты класса

Атрибуты класса - это атрибуты, которые принадлежат самому классу, а не его объектам.
Они могут быть инициализированы при его объявлении.
Атрибуты класса объявляются с помощью my вместо has.
Они вызываются для самого класса, а не его объектов.

class Human {
  has $.name;
  my $.counter = 0;
  method new($name) {
    Human.counter++;
    self.bless(:$name);
  }
}
my $a = Human.new('a');
my $b = Human.new('b');

say Human.counter;

9.6. Типы доступа

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

Что, если нам нужно модифицировать значение атрибута?
Нам нужно отметить его доступным для чтения/записи, используя ключевое слова is rw

class Human {
  has $.name;
  has $.age is rw;
}
my $john = Human.new(name => 'John', age => 21);
say $john.age;

$john.age = 23;
say $john.age;

По умолчанию все атрибуты объявлены только для чтения, но вы можете указать это явно, используя is readonly

9.7. Наследование

9.7.1. Введение

Наследование - это ещё одна концепция объектно-ориентированного программирования.

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

Предположим, что мы хотим объявить два класса: класс Human для людей и класс Employee для рабочих.
Люди имеют 2 атрибута: имя и возраст.
Рабочие имеют 4 атрибута: имя, возраст, компания и зарплата

Предположим, вы собираетесь определить классы так:

class Human {
  has $.name;
  has $.age;
}

class Employee {
  has $.name;
  has $.age;
  has $.company;
  has $.salary;
}

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

Лучшим вариантом написания такого будет:

class Human {
  has $.name;
  has $.age;
}

class Employee is Human {
  has $.company;
  has $.salary;
}

Ключевое слово is определяет наследование.
В объектно-ориентированном жаргоне мы говорим, что Employee это потомок Human, а Human это родительский класс Employee.

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

9.7.2. Переопределение

Классы наследуют все атрибуты и методы их родительских классов.
Однако, существуют случаи, когда мы хотим, чтобы метод потомка работал иначе, чем унаследованный.
Для того, чтобы это осуществить, мы переопределяем метод в классе-потомке.
Это называется переопределением.

В примере ниже, метод introduce-yourself унаследован классом Employee.

class Human {
  has $.name;
  has $.age;
  method introduce-yourself {
    say 'Hi I am a human being, my name is ' ~ self.name;
  }
}

class Employee is Human {
  has $.company;
  has $.salary;
}

my $john = Human.new(name =>'John', age => 23,);
my $jane = Employee.new(name =>'Jane', age => 25, company => 'Acme', salary => 4000);

$john.introduce-yourself;
$jane.introduce-yourself;

Переопределение работает так:

class Human {
  has $.name;
  has $.age;
  method introduce-yourself {
    say 'Hi I am a human being, my name is ' ~ self.name;
  }
}

class Employee is Human {
  has $.company;
  has $.salary;
  method introduce-yourself {
    say 'Hi I am a employee, my name is ' ~ self.name ~ ' and I work at: ' ~ self.company;
  }

}

my $john = Human.new(name =>'John',age => 23,);
my $jane = Employee.new(name =>'Jane',age => 25,company => 'Acme',salary => 4000);

$john.introduce-yourself;
$jane.introduce-yourself;

В зависимости от класса объекта, будет вызван нужный метод.

9.7.3. Подметоды

Подметоды - это тип методов, которые не наследуются классами-потомками.
Они доступны только из класса, где они определены.
Они определяются, используя ключевое слово submethod.

9.8. Множественное наследование

В Perl 6 доступно множественное наследование. Класс может быть потомком множества других классов.

class bar-chart {
  has Int @.bar-values;
  method plot {
    say @.bar-values;
  }
}

class line-chart {
  has Int @.line-values;
  method plot {
    say @.line-values;
  }
}

class combo-chart is bar-chart is line-chart {
}

my $actual-sales = bar-chart.new(bar-values => [10,9,11,8,7,10]);
my $forecast-sales = line-chart.new(line-values => [9,8,10,7,6,9]);

my $actual-vs-forecast = combo-chart.new(bar-values => [10,9,11,8,7,10],
                                         line-values => [9,8,10,7,6,9]);
say "Actual sales:";
$actual-sales.plot;
say "Forecast sales:";
$forecast-sales.plot;
say "Actual vs Forecast:";
$actual-vs-forecast.plot;
Вывод
Actual sales:
[10 9 11 8 7 10]
Forecast sales:
[9 8 10 7 6 9]
Actual vs Forecast:
[10 9 11 8 7 10]
Пояснение

Класс combo-chart должен содержать два ряда, один для значений, отображённых на гистограмме, и другой для прогнозируемых значений, построенных на линии.
Именно поэтому мы определили его как потомка line-chart и bar-chart.
Вы могли заметить, что вызов метода plot у combo-chart не возвращает желаемый результат. Только один ряд был построен.
Почему так произошло?
combo-chart наследует от line-chart и bar-chart, и у оба эти класса содержат метод с названием plot. Когда мы вызываем его для combo-chart, Raku попробует разрешить этот конфликт, вызвав один из унаследованных методов.

Исправление

Для того, чтобы выполняться корректно, нам следует переопределить метод plot для combo-chart.

class bar-chart {
  has Int @.bar-values;
  method plot {
    say @.bar-values;
  }
}

class line-chart {
  has Int @.line-values;
  method plot {
    say @.line-values;
  }
}

class combo-chart is bar-chart is line-chart {
  method plot {
    say @.bar-values;
    say @.line-values;
  }
}

my $actual-sales = bar-chart.new(bar-values => [10,9,11,8,7,10]);
my $forecast-sales = line-chart.new(line-values => [9,8,10,7,6,9]);

my $actual-vs-forecast = combo-chart.new(bar-values => [10,9,11,8,7,10],
                                         line-values => [9,8,10,7,6,9]);
say "Actual sales:";
$actual-sales.plot;
say "Forecast sales:";
$forecast-sales.plot;
say "Actual vs Forecast:";
$actual-vs-forecast.plot;
Вывод
Actual sales:
[10 9 11 8 7 10]
Forecast sales:
[9 8 10 7 6 9]
Actual vs Forecast:
[10 9 11 8 7 10]
[9 8 10 7 6 9]

9.9. Роли

Роли похожи на классы в том смысле, что являются наборами атрибутов и методов.

Роли объявляются с помощью ключевого слова role. Классы, которые реализовывают роль, делают это с помощью ключевого слова does.

Давайте перепишем пример с множественным наследованием, используя роли:
role bar-chart {
  has Int @.bar-values;
  method plot {
    say @.bar-values;
  }
}

role line-chart {
  has Int @.line-values;
  method plot {
    say @.line-values;
  }
}

class combo-chart does bar-chart does line-chart {
  method plot {
    say @.bar-values;
    say @.line-values;
  }
}

my $actual-sales = bar-chart.new(bar-values => [10,9,11,8,7,10]);
my $forecast-sales = line-chart.new(line-values => [9,8,10,7,6,9]);

my $actual-vs-forecast = combo-chart.new(bar-values => [10,9,11,8,7,10],
                                         line-values => [9,8,10,7,6,9]);
say "Actual sales:";
$actual-sales.plot;
say "Forecast sales:";
$forecast-sales.plot;
say "Actual vs Forecast:";
$actual-vs-forecast.plot;

Запустите скрипт выше и вы увидите, что результаты одинаковы.

Сейчас вы спрашиваете себя: если роли ведут себя как классы, в чём их польза?
Чтобы ответить на этот вопрос, измените первый скрипт, который показывает множественное наследование, так, будто мы забыли переопределить метод plot.

role bar-chart {
  has Int @.bar-values;
  method plot {
    say @.bar-values;
  }
}

role line-chart {
  has Int @.line-values;
  method plot {
    say @.line-values;
  }
}

class combo-chart does bar-chart does line-chart {
}

my $actual-sales = bar-chart.new(bar-values => [10,9,11,8,7,10]);
my $forecast-sales = line-chart.new(line-values => [9,8,10,7,6,9]);

my $actual-vs-forecast = combo-chart.new(bar-values => [10,9,11,8,7,10],
                                         line-values => [9,8,10,7,6,9]);
say "Actual sales:";
$actual-sales.plot;
say "Forecast sales:";
$forecast-sales.plot;
say "Actual vs Forecast:";
$actual-vs-forecast.plot;
Вывод
===SORRY!===
Method 'plot' must be resolved by class combo-chart because it exists in multiple roles (line-chart, bar-chart)
Пояснение

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

Роли предупредят вас, что существует конфликт.

9.10. Интроспекция

Интроспекция это процесс получения информации об объекте, такой как его тип, атрибуты или методы.

class Human {
  has Str $.name;
  has Int $.age;
  method introduce-yourself {
    say 'Hi I am a human being, my name is ' ~ self.name;
  }
}

class Employee is Human {
  has Str $.company;
  has Int $.salary;
  method introduce-yourself {
    say 'Hi I am a employee, my name is ' ~ self.name ~ ' and I work at: ' ~ self.company;
  }
}

my $john = Human.new(name =>'John',age => 23,);
my $jane = Employee.new(name =>'Jane',age => 25,company => 'Acme',salary => 4000);

say $john.WHAT;
say $jane.WHAT;
say $john.^attributes;
say $jane.^attributes;
say $john.^methods;
say $jane.^methods;
say $jane.^parents;
if $jane ~~ Human {say 'Jane is a Human'};

Интроспекция упрощается с помощью:

  • .WHAT — возвращает класс, с помощью которого был создан объект

  • .^attributes — возвращает все атрибуты объекта

  • .^methods — возвращает все методы, которые могут быть вызваны на объекте

  • .^parents — возвращает родительские классы объекта

  • ~~ называется оператором умного сравнения. В данном случае, он вычисляется как True, если объект создан из класса, с которым проводится сравнение, либо любого из его потомков.

Для того, чтобы узнать больше об объектно-ориентированном программировании в Raku, посетите:

10. Обработка исключений

10.1. Ловля исключений

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

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

my Str $name;
$name = "Joanna";
say "Hello " ~ $name;
say "How are you doing today?"
Вывод
Hello Joanna
How are you doing today?

Теперь рассмотрите скрипт, который выбрасывает исключение:

my Str $name;
$name = 123;
say "Hello " ~ $name;
say "How are you doing today?"
Вывод
Type check failed in assignment to $name; expected Str but got Int
   in block <unit> at exceptions.p6:2

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

Обработка исключений - это процесс ловли исключений, которые были выброшены, для того, чтобы скрипт продолжал работу.

my Str $name;
try {
  $name = 123;
  say "Hello " ~ $name;
  CATCH {
    default {
      say "Can you tell us your name again, we couldn't find it in the register.";
    }
  }
}
say "How are you doing today?";
Вывод
Can you tell us your name again, we couldn't find it in the register.
How are you doing today?

Исключения обрабатываются используя блок try-catch.

try {
  # код располагается здесь
  # если что-то пойдёт не так, скрипт начнёт выполнение блока CATCH ниже
  # если выполнение пройдёт нормально, блок CATCH будет проигнорирован
  CATCH {
    default {
      # код здесь будет выполняться, только если будет выброшено исключение
    }
  }
}

Блок CATCH может быть определён также, как блок given. Это означает, что мы можем ловить и обрабатывать по разному много типов исключений.

try {
  # код располагается здесь
  # если что-то пойдёт не так, скрипт начнёт выполнение блока CATCH ниже
  # если выполнение пройдёт нормально, блок CATCH будет проигнорирован
  CATCH {
    when X::AdHoc   { # сделать что-то, если выброшено исключение типа X::AdHoc }
    when X::IO      { # сделать что-то, если выброшено исключение типа X::IO }
    when X::OS      { # сделать что-то, если выброшено исключение типа X:OS }
    default         { # сделать что-то, если выброшенное исключение не принадлежит к одному из типов выше }
  }
}

10.2. Выброс исключений

В Raku можно выбрасывать исключение явно.
Два типа исключений могут быть выброшены:

  • ad-hoc исключения

  • типизированные исключения

ad-hoc
my Int $age = 21;
die "Error !";
типизированное
my Int $age = 21;
X::AdHoc.new(payload => 'Error !').throw;

Ad-hoc исключения создаются, используя подпрограмму die, которая принимает сообщение исключения.

Типизированные исключения это объекты, поэтому было необходимо использовать конструктор .new() в примере выше.
Все типизированные исключения являются потомками класса X , вот несколько примеров:
X::AdHoc это простейший тип исключения
X::IO относится к ошибкам ввода-вывода
X::OS относится к ошибкам ОС
X::Str::Numeric относится к попытке привести строку к числу

Для полного списка типов исключений и их методов, смотрите https://docs.raku.org/type-exceptions.html

11. Регулярные выражения

Регулярное выражение (англ. regular expressions, жарг. регэкспы или регексы) - это последовательность символов, которые используются для сравнения с образцом.
Регулярные выражения можно воспринимать как шаблон.

if 'enlightenment' ~~ m/ light / {
    say "enlightenment contains the word light";
}

В примере выше, оператор умного сравнения ~~ используется, чтобы проверить, содержит ли строка (enlightenment) слово (light).
"Enlightenment" сравнивается с регулярным выражением m/ light /

11.1. Определение регулярного выражения

Регулярное выражение можно определять так:

  • /light/

  • m/light/

  • rx/light/

Если не указано явно, символы пробела игнорируются; m/light/ и m/ light / означают одно и то же.

11.2. Символы сравнения с образцом

Алфавитные символы и цифры, а также нижнее подчёркивание _ означают сами себя.
Все остальные символы должны быть экранированы, используя обратную косую черту или взяты в кавычки.

Обратная косая черта
if 'Temperature: 13' ~~ m/ \: / {
    say "The string provided contains a colon :";
}
Одинарные кавычки
if 'Age = 13' ~~ m/ '=' / {
    say "The string provided contains an equal character = ";
}
Двойные кавычки
if 'name@company.com' ~~ m/ "@" / {
    say "This is a valid email address because it contains an @ character";
}

11.3. Сравнение с категорией символов

Символы могут быть классифицированы по категориям и мы можем сравнивать с ними.
Мы также можем сравнивать с инверсией этой категории (всё, кроме неё):

Категория

Регэксп

Инверсия

Регэксп

Символ слова (буква, цифра либо нижнее подчёркивание)

\w

Любой символ кроме символа слова

\W

Цифра

\d

Любой символ кроме цифры

\D

Пробел

\s

Любой символ кроме пробела

\S

Горизонтальный пробел

\h

Любой символ кроме горизонтального пробела

\H

Вертикальный пробел

\v

Любой символ кроме вертикального пробела

\V

Символ табуляции

\t

Любой символ кроме символа табуляции

\T

Перевод строки

\n

Любой символ кроме перевода строки

\N

if "John123" ~~ / \d / {
  say "This is not a valid name, numbers are not allowed";
} else {
  say "This is a valid name"
}
if "John-Doe" ~~ / \s / {
  say "This string contains whitespace";
} else {
  say "This string doesn't contain whitespace"
}

11.4. Свойства Юникода

Сравнение с категорией символов, как мы видели в предыдущем разделе, очень удобно.
При этом, более систематическим подходом будет использовать свойства (property) Юникода.
Это позволяет нам сравнивать с категорией символов, которые входят и не входят в стандарт ASCII.
Свойства Юникода заключаются в <: >

if "Devanagari Numbers १२३" ~~ / <:N> / {
  say "Contains a number";
} else {
  say "Doesn't contain a number"
}
if "Привет, Иван." ~~ / <:Lu> / {
  say "Contains an uppercase letter";
} else {
  say "Doesn't contain an upper case letter"
}
if "John-Doe" ~~ / <:Pd> / {
  say "Contains a dash";
} else {
  say "Doesn't contain a dash"
}

11.5. Символ подстановки

Также в регулярных выражениях можно использовать "символ подстановки" или "wildcard".

Точка . означает один любой символ.

if 'abc' ~~ m/ a.c / {
    say "Match";
}
if 'a2c' ~~ m/ a.c / {
    say "Match";
}
if 'ac' ~~ m/ a.c / {
    say "Match";
} else {
    say "No Match";
}

11.6. Кванторы

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

Знак вопроса ? означает ноль или один раз.

if 'ac' ~~ m/ a?c / {
    say "Match";
} else {
    say "No Match";
}
if 'c' ~~ m/ a?c / {
    say "Match";
} else {
    say "No Match";
}

Астериск (звёздочка) * означает ноль или больше раз.

if 'az' ~~ m/ a*z / {
    say "Match";
} else {
    say "No Match";
}
if 'aaz' ~~ m/ a*z / {
    say "Match";
} else {
    say "No Match";
}
if 'aaaaaaaaaaz' ~~ m/ a*z / {
    say "Match";
} else {
    say "No Match";
}
if 'z' ~~ m/ a*z / {
    say "Match";
} else {
    say "No Match";
}

+ означает как минимум один раз.

if 'az' ~~ m/ a+z / {
    say "Match";
} else {
    say "No Match";
}
if 'aaz' ~~ m/ a+z / {
    say "Match";
} else {
    say "No Match";
}
if 'aaaaaaaaaaz' ~~ m/ a+z / {
    say "Match";
} else {
    say "No Match";
}
if 'z' ~~ m/ a+z / {
    say "Match";
} else {
    say "No Match";
}

11.7. Результат сравнения

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

Скрипт
if 'Rakudo is a Raku compiler' ~~ m/:s Raku/ {
    say "The match is: " ~ $/;
    say "The string before the match is: " ~ $/.prematch;
    say "The string after the match is: " ~ $/.postmatch;
    say "The matching string starts at position: " ~ $/.from;
    say "The matching string ends at position: " ~ $/.to;
}
Вывод
The match is: Raku
The string before the match is: Rakudo is a
The string after the match is: compiler
The matching string starts at position: 12
The matching string ends at position: 18
Пояснение

$/ возвращает объект Match (строку, которая отвечает регулярному выражению)
Вот часть методов, определённых для объекта Match:
.prematch возвращает строку, предшествующую совпадению.
.postmatch возвращает строку, идущую после совпадения.
.from возвращает начальную позицию совпадения.
.to возвращает конечную позицию совпадения.

По умолчанию, пробелы в регулярных выражениях игнорируются.
Если мы хотим сравнивать с регулярным выражением, явно содержащим символы пробела, мы должны делать это явно.
Использование :s в регулярном выражении m/:s Raku/ позволяет учитывать символы пробела.
Другой вариант - мы могли написать регулярное выражение как m/ Raku\s6 / и использовать \s, который означает пробел.
Если регулярное выражение содержит больше одного пробела, использование :s более предпочтительно, чем запись \s для каждого пробела.

11.8. Пример

Давайте проверим, корректный ли электронный адрес или нет.
Для простоты примера мы предположим, что корректный электронный адрес имеет формат:
имя [dot] фамилия [at] компания [dot] (com/org/net)

Регулярное выражение, использованное в этом примере для проверки электронного адреса, не очень точное.
Его единственное применение это продемонстрировать регулярные выражения в Raku.
Не используйте его без изменений в настоящих программах.
Скрипт
my $email = 'john.doe@perl6.org';
my $regex = / <:L>+\.<:L>+\@<:L+:N>+\.<:L>+ /;

if $email ~~ $regex {
  say $/ ~ " is a valid email";
} else {
  say "This is not a valid email";
}
Вывод

john.doe@perl6.org is a valid email

Пояснение

<:L> совпадает с одной буквой
<:L>` совпадает с одной или больше буквами + `\.` совпадает с одним символом [dot] + `\@` совпадает с одним символом [at] + `<:L:N> совпадает с буквой или цифрой
<:L+:N>+ совпадает с одной или более буквой либо цифрой

Это регулярное выражение можно разделить на части так:

  • имя <:L>+

  • [dot] \.

  • фамилия <:L>+

  • [at] \@

  • компания <:L+:N>+

  • [dot] \.

  • com/org/net <:L>+

Как вариант, регулярное выражение может быть разбито на множество именованных регулярных выражений
my $email = 'john.doe@perl6.org';
my regex many-letters { <:L>+ };
my regex dot { \. };
my regex at { \@ };
my regex many-letters-numbers { <:L+:N>+ };

if $email ~~ / <many-letters> <dot> <many-letters> <at> <many-letters-numbers> <dot> <many-letters> / {
  say $/ ~ " is a valid email";
} else {
  say "This is not a valid email";
}

Именованный регэксп определяется, используя следующий синтаксис: my regex regex-name { regex definition }
Именованный регэксп может быть использован, используя следующий синтаксис: <regex-name>

Больше о регэкспах в Raku можно узнать на https://docs.raku.org/language/regexes

12. Модули Raku

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

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

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

Zef - это средство для управления модулями, которое поставляется вместе с Rakudo Star.

Чтобы установить конкретный модуль, выполните команду ниже в эмуляторе терминала:

zef install "имя модуля"

Список модулей Raku можно найти на https://modules.raku.org/

12.1. Использование модулей

MD5 это криптографическая хеш-функция, результатом которой является 128-битное значение хеша.
У MD5 есть множество применений в приложениях, включая шифрование паролей, хранящихся в базе данных. Когда новый пользователь регистрируется, его данные хранятся не в чистом виде, а в хешированом. Идея заключается в том, что если база данных будет скомпрометирована, атакующий не сможет узнать пароли.

К счастью, нам не нужно реализовывать алгоритм MD5 вручную; существует модуль, написанный на Raku, где это реализовано.

Давайте установим его:
zef install Digest::MD5

Теперь, запустите скрипт ниже:

use Digest::MD5;
my $password = "password123";
my $hashed-password = Digest::MD5.new.md5_hex($password);

say $hashed-password;

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

На практике, само по себе MD5 хеширование не является достаточным, потому что легко уязвимо к атакам по словарю.
Его следует комбинировать с солью https://ru.wikipedia.org/wiki/%D0%A1%D0%BE%D0%BB%D1%8C_(%D0%BA%D1%80%D0%B8%D0%BF%D1%82%D0%BE%D0%B3%D1%80%D0%B0%D1%84%D0%B8%D1%8F).

13. Юникод

Юникод это стандарт для кодирования и представления текста для большинства систем письменности в мире.
UTF-8 это символьная кодировка, с помощью которой можно кодировать все возможные символы (или "коды символов") в Юникоде.

Символы в нём определяются через:
Графема: Визуальное отображение.
Код символа: Число, присвоенное символу.
Имя кода символа: Имя присвоенное символу.

13.1. Использование Юникода

Рассмотрим, как мы можем выводить символы, используя Юникод
say "a";
say "\x0061";
say "\c[LATIN SMALL LETTER A]";

Три строчки выше являются примером разных способов построения символа:

  1. Написание символа напрямую (графемы)

  2. Использование \x и кода символа

  3. Использование \c и имени кода символа

Теперь давайте выведем смайлик
say "☺";
say "\x263a";
say "\c[WHITE SMILING FACE]";
Пример соединения двух кодов символов
say "á";
say "\x00e1";
say "\x0061\x0301";
say "\c[LATIN SMALL LETTER A WITH ACUTE]";

Буква á может быть написана:

  • используя её уникальный код символа \x00e1

  • либо как комбинация кодов символов a и острого ударения \x0061\x0301

Несколько методов, которые можно вызывать:
say "á".NFC;
say "á".NFD;
say "á".uniname;
Вывод
NFC:0x<00e1>
NFD:0x<0061 0301>
LATIN SMALL LETTER A WITH ACUTE

NFC возвращает уникальный код символа.
NFD декомпозирует символ и возвращает коды символов всех частей.
uniname возвращает имя кода символа.

Буквы юникода могут быть использованы в качестве идентификаторов:
my  = 1;
++;
say ;
С помощью Юникода можно считать:
my $var = 2 + ;
say $var;

13.2. Операции, которые учитывают Юникод

13.2.1. Числа

Арабские числа состоят из десяти цифр: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9. Такой числовой набор самый распространённый в мире.

Невзирая на это, разные цифры используются в разных частях мира, хотя и с меньшей распространённостью.

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

say (٤,٥,٦,1,2,3).sort; # (1 2 3 4 5 6)
say 1 + ٩;              # 10

13.2.2. Строки

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

Сравнения
say 'a' cmp 'B'; # More

Сравнение выше показывает, что a больше, чем B. Причина в том, что код символа прописной a больше, чем код символа заглавной B.

Хотя это технически корректно, такое поведение не всегда желательно.

К счастью, в Raku есть методы и операции, которые реализуют Unicode Collation Algorithm.
Один из них это unicmp, который повторяет показанный выше cmp, но с учётом Юникода.

say 'a' unicmp 'B'; # Less

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

Сортировка

Как альтернатива методу sort, который сортирует на основе кодов символов, в Raku есть метод collate, который реализует Unicode Collation Algorithm.

say ('a','b','c','D','E','F').sort;    # (D E F a b c)
say ('a','b','c','D','E','F').collate; # (a b c D E F)

14. Параллелизм, одновременность и асинхронность

14.1. Параллелизм

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

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

  • Параллелизм задач: Два (или больше) независимых выражения вычисляются параллельно.

  • Параллелизм данных: Единственное выражение обходит список элементов параллельно.

Давайте начнём со второго.

14.1.1. Параллелизм данных

my @array = (0..50000);                     # Наполнение массива
my @result = @array.map({ is-prime $_ });   # вызов is-prime для каждого элемента массива
say now - INIT now;                         # Вывод времени, затраченного на выполнение скрипта
Рассматривая пример выше:

Мы делаем только одну операцию @array.map({ is-prime $_ })
Подпрограмма is-prime вызывается для каждого элемента массива последовательно:
is-prime @array[0], потом is-prime @array[1], потом is-prime @array[2] и так далее.

К счастью, мы можем вызвать is-prime на нескольких элементах массива одновременно:
my @array = (0..50000);                         # Наполнение массива
my @result = @array.race.map({ is-prime $_ });  # вызов is-prime для каждого элемента массива
say now - INIT now;                             # Вывод времени, затраченного на выполнение скрипта

Отметьте использование race в выражении. Этот метод включает параллельный обход элементов массива.

После запуска каждого примера (с и без race), сравните время, которое потребовалось для выполнения каждого скрипта.

race не сохраняет порядок элементов. Если это необходимо, вместо него используйте hyper.

race
my @array = (1..1000);
my @result = @array.race.map( {$_ + 1} );
.say for @result;
hyper
my @array = (1..1000);
my @result = @array.hyper.map( {$_ + 1} );
.say for @result;

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

14.1.2. Параллелизм задач

my @array1 = (0..49999);
my @array2 = (2..50001);

my @result1 = @array1.map( {is-prime($_ + 1)} );
my @result2 = @array2.map( {is-prime($_ - 1)} );

say @result1 eqv @result2;

say now - INIT now;
Рассмотрим пример выше:
  1. Мы определили два массива

  2. применили разные операции к каждому массиву и сохранили результаты

  3. проверили, являются ли оба результата одинаковыми

Скрипт ждёт, пока выполнится @array1.map( {is-prime($_ + 1)} ),
а потом выполняет @array2.map( {is-prime($_ - 1)} )

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

Почему бы не сделать их параллельными?
my @array1 = (0..49999);
my @array2 = (2..50001);

my $promise1 = start @array1.map( {is-prime($_ + 1)} ).eager;
my $promise2 = start @array2.map( {is-prime($_ - 1)} ).eager;

my @result1 = await $promise1;
my @result2 = await $promise2;

say @result1 eqv @result2;

say now - INIT now;
Пояснение

Подпрограмма start выполняет код и возвращает объект типа Promise или кратко промис (обещание).
Если код выполнится корректно, промис будет сдержан.
Если код выбросит исключение, промис будет нарушен.

Подпрограмма await ждёт промис.
Если оно сдержано, то результат будут возвращён.
Если оно нарушено, будет брошено исключение.

Проверьте время, которое заняло выполнение каждого скрипта.

Параллелизм всегда добавляет дополнительную нагрузку ("оверхед") из-за многопоточности. Если эта нагрузка не покрывается приростом скорости вычисления, скрипт будет работать медленнее.
Именно поэтому, использование race, hyper, start и await для очень простых скриптов на деле может сделать их медленнее.

14.2. Одновременность и Асинхронность

Больше узнать об одновременности и асинхронности в программировании можно узнать на https://docs.raku.org/language/concurrency

15. Интерфейс для нативных вызовов

В Raku есть возможность использовать библиотеки языка Си, используя интерфейс для нативных вызовов.

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

15.1. Вызов функции

Рассмотрим код на языке Си, который определяет функцию под названием hellofromc. Эта функция выводит в терминал Hello from C. Она не принимает никаких аргументов и не возвращает никакого значения.

ncitest.c
#include <stdio.h>

void hellofromc () {
  printf("Hello from C\n");
}

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

Для Linux:
gcc -c -fpic ncitest.c
gcc -shared -o libncitest.so ncitest.o
Для Windows:
gcc -c ncitest.c
gcc -shared -o ncitest.dll ncitest.o
Для macOS:
gcc -dynamiclib -o libncitest.dylib ncitest.c

В той же директории, где вы скомпилировали Си библиотеку, создайте новый скрипт на Raku, который содержит код ниже, и запустите его.

ncitest.p6
use NativeCall;

constant LIBPATH = "$*CWD/ncitest";
sub hellofromc() is native(LIBPATH) { * }

hellofromc();
Пояснение

Сперва мы объявили, что будем использовать модуль NativeCall.
После этого мы создали константу LIBPATH, которая содержит путь к Си библиотеке.
Отметьте, что $*CWD возвращает текущую рабочую директорию.
После этого мы создали подпрограмму под названием hellofromc(), которая служит обёрткой для своей дополняющей функции на Си, с таким же именем и находящуюся в Си библиотеке, которую можно найти в пути LIBPATH.
Всё это сделано используя трейт is native.
Наконец, мы вызываем нашу подпрограмму в Raku.

В сущности, всё сводится к объявлению подпрограммы с трейтом is native и именем Си библиотеки.

15.2. Переименование функции

В подразделе выше, мы видели, как можно вызвать очень простую функцию на Си, обернув её с помощью подпрограммы в Raku с таким же именем, используя трейт is native.

В некоторых случаях, может быть предпочтительно изменить имя подпрограммы Raku.
Для этого, мы используем трейт is symbol.

Давайте изменим скрипт выше на Raku и назовём Raku подпрограмму hello вместо hellofromc

ncitest.p6
use NativeCall;

constant LIBPATH = "$*CWD/ncitest";
sub hello() is native(LIBPATH) is symbol('hellofromc') { * }

hello();
Пояснение

В случае, когда подпрограмма в Raku имеет имя отличное от своего Си дополнения, нам следует использовать трейт is symbol с именем оригинальной функции на Си.

15.3. Передача аргументов

Скомпилируйте код модифицированной библиотеки Си и запустите скрипт на Raku, которые указаны ниже.
Отметьте, что мы изменили и код на Си и код на Raku для того, чтобы принимать аргументом строку (char* в Cи и Str в Raku)

ncitest.c
#include <stdio.h>

void hellofromc (char* name) {
  printf("Hello, %s! This is C!\n", name);
}
ncitest.p6
use NativeCall;

constant LIBPATH = "$*CWD/ncitest";
sub hello(Str) is native(LIBPATH) is symbol('hellofromc') { * }

hello('Jane');

15.4. Возврат значений

Повторим процесс, создав простой калькулятор, который принимает 2 целых числа и складывает их.
Скомпилируйте Си библиотеку и запустите Raku скрипт.

ncitest.c
int add (int a, int b) {
  return (a + b);
}
ncitest.p6
use NativeCall;

constant LIBPATH = "$*CWD/ncitest";
sub add(int32,int32) returns int32 is native(LIBPATH) { * }

say add(2,3);

Отметьте, что и функция на Си и функция на Raku принимают два целых числа и возвращают одно (int в Cи и int32 в Raku)

15.5. Типы

Возможно, вы задаётесь вопросом, почему мы использовали int32 вместо Int в последнем скрипте на Raku.
Часть типов в Raku, такие как Int, Rat и некоторые другие, не могут передаваться или быть возвращены значением из функции на Cи.

Необходимо использовать такие же типы в Raku, как в Cи.

К счастью, Raku предоставляет много типов для того, чтобы представлять соответствующий тип в Cи.

Тип в Cи Тип в Perl 6

char

int8

int8_t

short

int16

int16_t

int

int32

int32_t

int64_t

int64

unsigned char

uint8

uint8_t

unsigned short

uint16

uint16_t

unsigned int

uint32

uint32_t

uint64_t

uint64

long

long

long long

longlong

float

num32

double

num64

size_t

size_t

bool

bool

char* (String)

Str

Массивы: к примеру, int* (массив целых чисел) и double* (массив чисел с удвоенной точностью)

CArray: к примеру, CArray[int32] и CArray[num64]

Больше об интерфейсе для нативных вызовов можно узнать на https://docs.raku.org/language/nativecall

16. Сообщество

  • #raku IRC канал. Много вещей обсуждается на IRC. Скорее всего, это первое место, куда вам следует обращаться по любому вопросу: https://raku.org/community/irc

  • rakudoweekly это еженедельный обзор изменений в Raku и связанных с ним областях.

  • pl6anet агрегатор блогов. Оставайтесь в курсе, читая блоги, которые сфокусированы на Raku.

  • /r/rakulang/ подписывайтесь на подраздел Raku.

  • @perl6org следите за нами в Twitter.

  • StackOverflow Raku questions Место где на Ваш вопрос могут ответить более развернуто


1. Notations for intervals: https://en.wikipedia.org/wiki/Interval_(mathematics)#Notations_for_intervals