Эликсир — Обзор
Elixir — это динамический, функциональный язык, разработанный для создания масштабируемых и поддерживаемых приложений. Он использует виртуальную машину Erlang, известную своими низкоуровневыми распределенными и отказоустойчивыми системами, а также успешно используется в веб-разработке и в области встроенного программного обеспечения.
Elixir — это функциональный динамический язык, созданный на основе Erlang и Erlang VM. Erlang — это язык, который был изначально написан компанией Ericsson в 1986 году для решения таких проблем телефонии, как распределение, отказоустойчивость и параллелизм. Эликсир, написанный Хосе Валимом, расширяет Erlang и предоставляет дружественный синтаксис в Erlang VM. Это достигается при сохранении производительности на том же уровне, что и Erlang.
Особенности эликсира
Давайте теперь обсудим несколько важных особенностей эликсира —
-
Масштабируемость — весь код Elixir выполняется внутри легких процессов, которые изолированы и обмениваются информацией посредством сообщений.
-
Отказоустойчивость — Elixir предоставляет супервизоры, которые описывают, как перезапускать части вашей системы, когда что-то идет не так, возвращаясь к известному начальному состоянию, которое гарантированно будет работать. Это гарантирует, что ваше приложение / платформа никогда не выйдет из строя.
-
Функциональное программирование. Функциональное программирование поддерживает стиль кодирования, который помогает разработчикам писать короткий, быстрый и понятный код.
-
Инструменты сборки — Elixir поставляется с набором инструментов разработки. Mix является одним из таких инструментов, который позволяет легко создавать проекты, управлять задачами, запускать тесты и т. Д. Он также имеет свой собственный менеджер пакетов — Hex.
-
Совместимость с Erlang — Elixir работает на Erlang VM, предоставляя разработчикам полный доступ к экосистеме Erlang.
Масштабируемость — весь код Elixir выполняется внутри легких процессов, которые изолированы и обмениваются информацией посредством сообщений.
Отказоустойчивость — Elixir предоставляет супервизоры, которые описывают, как перезапускать части вашей системы, когда что-то идет не так, возвращаясь к известному начальному состоянию, которое гарантированно будет работать. Это гарантирует, что ваше приложение / платформа никогда не выйдет из строя.
Функциональное программирование. Функциональное программирование поддерживает стиль кодирования, который помогает разработчикам писать короткий, быстрый и понятный код.
Инструменты сборки — Elixir поставляется с набором инструментов разработки. Mix является одним из таких инструментов, который позволяет легко создавать проекты, управлять задачами, запускать тесты и т. Д. Он также имеет свой собственный менеджер пакетов — Hex.
Совместимость с Erlang — Elixir работает на Erlang VM, предоставляя разработчикам полный доступ к экосистеме Erlang.
Эликсир — Окружающая среда
Чтобы запустить Elixir, вам нужно настроить его локально в вашей системе.
Чтобы установить Elixir, вам сначала потребуется Erlang. На некоторых платформах пакеты Elixir поставляются с Erlang.
Установка Эликсира
Давайте теперь разберемся с установкой Elixir в разных операционных системах.
Программа установки Windows
Чтобы установить Elixir в Windows, загрузите установщик с https://repo.hex.pm/elixirwebsetup.exe и просто нажмите « Далее», чтобы выполнить все шаги. У вас будет это в вашей локальной системе.
Если у вас возникли проблемы при установке, вы можете проверить эту страницу для получения дополнительной информации.
Настройка Mac
Если у вас установлен Homebrew, убедитесь, что это последняя версия. Для обновления используйте следующую команду —
brew update
Теперь установите Elixir, используя приведенную ниже команду —
brew install elixir
Настройка Ubuntu / Debian
Чтобы установить Elixir в настройках Ubuntu / Debian, выполните следующие действия:
Добавить репозиторий Erlang Solutions —
wget https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb && sudo dpkg -i erlang-solutions_1.0_all.deb sudo apt-get update
Установите платформу Erlang / OTP и все ее приложения —
sudo apt-get install esl-erlang
Установить Эликсир —
sudo apt-get install elixir
Другие дистрибутивы Linux
Если у вас есть какой-либо другой дистрибутив Linux, перейдите на эту страницу, чтобы настроить elixir в вашей локальной системе.
Тестирование установки
Чтобы проверить настройки Elixir в вашей системе, откройте ваш терминал и введите в него iex. Откроется интерактивная оболочка эликсира, как показано ниже:
Erlang/OTP 19 [erts-8.0] [source-6dc93c1] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false] Interactive Elixir (1.3.1) - press Ctrl+C to exit (type h() ENTER for help) iex(1)>
Elixir успешно установлен в вашей системе.
Эликсир — основной синтаксис
Мы начнем с обычной программы «Hello World».
Чтобы запустить интерактивную оболочку Elixir, введите следующую команду.
iex
После запуска оболочки используйте функцию IO.puts, чтобы «поместить» строку в вывод консоли. Введите следующее в вашей оболочке Elixir —
IO.puts "Hello world"
В этом руководстве мы будем использовать режим сценариев Elixir, в котором мы будем хранить код Elixir в файле с расширением .ex . Давайте теперь сохраним приведенный выше код в файле test.ex. На следующем шаге мы выполним его с помощью elixirc —
IO.puts "Hello world"
Давайте теперь попробуем запустить вышеуказанную программу следующим образом:
$elixirc test.ex
Вышеуказанная программа генерирует следующий результат —
Hello World
Здесь мы вызываем функцию IO.puts для генерации строки в нашу консоль в качестве вывода. Эту функцию также можно назвать так, как мы это делаем в C, C ++, Java и т. Д., Предоставляя аргументы в скобках после имени функции —
IO.puts("Hello world")
Комментарии
Однострочные комментарии начинаются с символа «#». Там нет многострочного комментария, но вы можете сложить несколько комментариев. Например —
#This is a comment in Elixir
Концы строк
Нет обязательных окончаний строки, таких как ‘;’ в эликсире. Тем не менее, мы можем иметь несколько операторов в одной строке, используя ‘;’. Например,
IO.puts("Hello"); IO.puts("World!")
Вышеуказанная программа генерирует следующий результат —
Hello World!
Идентификаторы
Идентификаторы, такие как переменные, имена функций, используются для идентификации переменной, функции и т. Д. В Elixir вы можете называть свои идентификаторы, начиная с букв нижнего регистра с цифрами, подчеркиваниями и заглавными буквами после них. Это соглашение об именах широко известно как snake_case. Например, ниже приведены некоторые действительные идентификаторы в Elixir —
var1 variable_2 one_M0r3_variable
Обратите внимание, что переменные также могут быть названы с начальным подчеркиванием. Значение, которое не предназначено для использования, должно быть присвоено _ или переменной, начинающейся с подчеркивания —
_some_random_value = 42
Также elixir опирается на подчеркивания, чтобы сделать функции приватными для модулей. Если вы назовете функцию с начальным подчеркиванием в модуле и импортируете этот модуль, эта функция не будет импортирована.
В Elixir есть еще много тонкостей, связанных с именованием функций, которые мы обсудим в следующих главах.
Зарезервированные слова
Следующие слова зарезервированы и не могут использоваться в качестве переменных, имен модулей или функций.
after and catch do inbits inlist nil else end not or false fn in rescue true when xor __MODULE__ __FILE__ __DIR__ __ENV__ __CALLER__
Эликсир — Типы данных
Для использования любого языка вам необходимо понимать основные типы данных, которые поддерживает этот язык. В этой главе мы обсудим 7 основных типов данных, поддерживаемых языком эликсира: целые числа, числа с плавающей точкой, логические значения, атомы, строки, списки и кортежи.
Числовые Типы
Elixir, как и любой другой язык программирования, поддерживает как целые числа, так и числа с плавающей точкой. Если вы откроете оболочку elixir и введете любое целое число или число с плавающей запятой в качестве входных данных, она вернет свое значение. Например,
42
Когда вышеуказанная программа запущена, она дает следующий результат —
42
Вы также можете определить числа в восьмеричном, шестнадцатеричном и двоичном основаниях.
восьмеричный
Чтобы определить число в восьмеричной базе, поставьте перед ним «0o». Например, 0o52 в восьмеричном эквиваленте равно 42 в десятичном.
шестнадцатеричный
Чтобы определить число в десятичной базе, добавьте к нему префикс «0x». Например, 0xF1 в шестнадцатеричном формате эквивалентно 241 в десятичном виде.
двоичный
Чтобы определить число в двоичной базе, добавьте к нему префикс «0b». Например, 0b1101 в двоичном коде эквивалентно 13 в десятичном виде.
Elixir поддерживает 64-битную двойную точность для чисел с плавающей запятой. И они также могут быть определены с использованием стиля возведения в степень. Например, 10145230000 можно записать как 1.014523e10
атомы
Атомы — это константы, чье имя является их ценностью. Их можно создать с помощью символа цвета (:). Например,
:hello
Булевы
Эликсир поддерживает true и false как Booleans. Оба эти значения на самом деле привязаны к атомам: true и: false соответственно.
Струны
Строки в Elixir вставляются между двойными кавычками и кодируются в UTF-8. Они могут занимать несколько строк и содержать интерполяции. Чтобы определить строку, просто введите ее в двойных кавычках —
"Hello world"
Для определения многострочных строк мы используем синтаксис, похожий на python, с тройными двойными кавычками —
"""
Hello
World!
"""
Мы подробно узнаем о строках, двоичных файлах и списках символов (аналогично строкам) в главе о строках.
Бинарные
Двоичные файлы — это последовательности байтов, заключенные в << >>, разделенные запятой. Например,
<< 65, 68, 75>>
Двоичные файлы в основном используются для обработки данных, связанных с битами и байтами, если они у вас есть. По умолчанию они могут хранить от 0 до 255 в каждом значении. Этот предел размера может быть увеличен с помощью функции размера, которая сообщает, сколько битов нужно взять, чтобы сохранить это значение. Например,
<<65, 255, 289::size(15)>>
Списки
Elixir использует квадратные скобки для указания списка значений. Значения могут быть любого типа. Например,
[1, "Hello", :an_atom, true]
Списки поставляются со встроенными функциями для заголовка и хвоста списка с именами hd и tl, которые возвращают заголовок и хвост списка соответственно. Иногда, когда вы создаете список, он возвращает список символов. Это потому, что когда elixir видит список печатных символов ASCII, он печатает его как список символов. Обратите внимание, что строки и списки символов не равны. Мы обсудим списки в следующих главах.
Кортеж
Elixir использует фигурные скобки для определения кортежей. Как и списки, кортежи могут содержать любое значение.
{ 1, "Hello", :an_atom, true
Здесь возникает вопрос: зачем указывать списки и кортежи, если они работают одинаково? Ну, у них есть разные реализации.
-
Списки на самом деле хранятся в виде связанных списков, поэтому вставки, удаления в списках выполняются очень быстро.
-
Кортежи, с другой стороны, хранятся в непрерывном блоке памяти, что ускоряет доступ к ним, но увеличивает затраты на вставки и удаления.
Списки на самом деле хранятся в виде связанных списков, поэтому вставки, удаления в списках выполняются очень быстро.
Кортежи, с другой стороны, хранятся в непрерывном блоке памяти, что ускоряет доступ к ним, но увеличивает затраты на вставки и удаления.
Эликсир — Переменные
Переменная предоставляет нам именованное хранилище, которым наши программы могут манипулировать. Каждая переменная в Elixir имеет определенный тип, который определяет размер и расположение памяти переменной; диапазон значений, которые могут быть сохранены в этой памяти; и набор операций, которые могут быть применены к переменной.
Типы переменных
Elixir поддерживает следующие основные типы переменных.
целое число
Они используются для целых чисел. Они имеют размер 32 бита в 32-битной архитектуре и 64 бита в 64-битной архитектуре. Целые числа всегда подписываются в эликсире. Если целое число начинает увеличиваться в размере, превышающем его предел, эликсир переводит его в большое целое число, которое занимает память в диапазоне от 3 до n слов, в зависимости от того, что может поместиться в памяти.
Поплавки
Поплавки имеют 64-битную точность в эликсире. Они также как целые числа с точки зрения памяти. При определении числа с плавающей запятой можно использовать экспоненциальную запись.
логический
Они могут принимать 2 значения, которые являются либо истиной, либо ложью.
Струны
Строки utf-8 закодированы в эликсире. У них есть модуль строк, который предоставляет программисту множество функций для работы со строками.
Анонимные функции / Лямбды
Это функции, которые можно определить и присвоить переменной, которую затем можно использовать для вызова этой функции.
Коллекции
Есть много типов коллекций, доступных в эликсире. Некоторые из них — это списки, кортежи, карты, двоичные файлы и т. Д. Они будут обсуждаться в последующих главах.
Объявление переменной
Объявление переменной сообщает интерпретатору, где и сколько нужно создать хранилище для переменной. Эликсир не позволяет нам просто объявить переменную. Переменная должна быть объявлена и ей присвоено значение одновременно. Например, чтобы создать переменную с именем life и присвоить ей значение 42, мы делаем следующее —
life = 42
Это свяжет срок службы переменной со значением 42. Если мы хотим переназначить эту переменную новым значением, мы можем сделать это, используя тот же синтаксис, что и выше, т.е.
life = "Hello world"
Именование переменных
Переменные именования следуют соглашению snake_case в Elixir, т. Е. Все переменные должны начинаться со строчной буквы, за которой следуют 0 или более букв (как в верхнем, так и в нижнем регистре), после которых в конце указывается необязательный ‘?’ ИЛИ ЖЕ ‘!’.
Имена переменных также могут начинаться с начального подчеркивания, но их следует использовать только при игнорировании переменной, т. Е. Эта переменная больше не будет использоваться, но ее необходимо присвоить чему-либо.
Переменные печати
В интерактивной оболочке переменные будут напечатаны, если вы просто введете имя переменной. Например, если вы создаете переменную —
life = 42
И введите «жизнь» в вашей оболочке, вы получите вывод как —
42
Но если вы хотите вывести переменную на консоль (при запуске внешнего скрипта из файла), вам необходимо предоставить переменную в качестве входных данных для функции IO.puts —
life = 42 IO.puts life
или же
life = 42 IO.puts(life)
Это даст вам следующий результат —
42
Эликсир — Операторы
Оператор — это символ, который указывает компилятору выполнять определенные математические или логические манипуляции. Есть много операторов, предоставляемых эликсиром. Они делятся на следующие категории —
- Арифметические операторы
- Операторы сравнения
- Булевы операторы
- Разные операторы
Арифметические Операторы
В следующей таблице приведены все арифметические операторы, поддерживаемые языком Elixir. Предположим, что переменная A содержит 10, а переменная B содержит 20, тогда —
оператор | Описание | пример |
---|---|---|
+ | Добавляет 2 номера. | А + Б даст 30 |
— | Вычитает второе число из первого. | АБ даст -10 |
* | Умножает два числа. | А * Б даст 200 |
/ | Делит первое число на второе. Это бросает числа в числах с плавающей точкой и дает результат числа с плавающей точкой | А / Б даст 0,5. |
ДИВ | Эта функция используется для получения отношения деления. | div (10,20) даст 0 |
рем | Эта функция используется для получения остатка от деления. | rem (A, B) даст 10 |
Операторы сравнения
Операторы сравнения в Elixir чаще всего встречаются в большинстве других языков. В следующей таблице приведены операторы сравнения в Elixir. Предположим, что переменная A содержит 10, а переменная B содержит 20, тогда —
оператор | Описание | пример |
---|---|---|
== | Проверяет, равно ли значение слева значению справа (тип сбрасывает значения, если они не одного типа). | A == B даст ложное |
знак равно | Проверяет, не равно ли значение слева значению справа. | A! = B даст истинное |
=== | Проверяет, равен ли тип значения слева типу значения справа, если да, то проверьте то же самое для значения. | A === B даст ложное |
==! | То же, что и выше, но проверяет неравенство вместо равенства. | А! == Б даст правду |
> | Проверяет, больше ли значение левого операнда, чем значение правого операнда; если да, то условие становится истинным. | A> B даст ложное |
< | Проверяет, меньше ли значение левого операнда, чем значение правого операнда; если да, то условие становится истинным. | А <Б даст правду |
> = | Проверяет, больше ли значение левого операнда или равно значению правого операнда; если да, то условие становится истинным. | A> = B даст ложное |
<= | Проверяет, меньше ли значение левого операнда или равно значению правого операнда; если да, то условие становится истинным. | A <= B даст истинное |
Логические операторы
Elixir предоставляет 6 логических операторов: и, или, нет, &&, || а также !. Первые три, или нет, являются строгими логическими операторами, что означает, что они ожидают, что их первый аргумент будет логическим. Не логический аргумент вызовет ошибку. В то время как следующие три, &&, || а также ! не являются строгими, не требуют, чтобы мы имели первое значение строго как логическое значение. Они работают так же, как их строгие коллеги. Предположим, что переменная A истинна, а переменная B содержит 20, тогда
оператор | Описание | пример |
---|---|---|
а также | Проверяет, верны ли оба предоставленных значения, если да, то возвращает значение второй переменной. (Логично и). | А и Б дадут 20 |
или же | Проверяет, является ли любое из указанных значений правдивым. Возвращает, какое бы значение не было правдивым. Остальное возвращает ложь. (Логично или). | А или Б даст истинное |
не | Унарный оператор, который инвертирует значение заданного входа. | не А даст ложный |
&& | Не строгий и Работает так же, как и, но не ожидает, что первый аргумент будет логическим. | B && A даст 20 |
|| | Нестрогий или . Работает так же, как или, но не ожидает, что первый аргумент будет логическим. | B || А даст правду |
! | Не строгий нет . Работает так же, как и нет, но не ожидает, что аргумент будет логическим. | ! А даст ложное |
ПРИМЕЧАНИЕ — и , или , && и || || являются операторами короткого замыкания. Это означает, что если первый аргумент и равен false, то он больше не будет проверять второй. И если первый аргумент или является истинным, то он не будет проверять второй. Например,
false and raise("An error") #This won't raise an error as raise function wont get executed because of short #circuiting nature of and operator
Битовые операторы
Побитовые операторы работают с битами и выполняют побитовые операции. Elixir предоставляет побитовые модули как часть пакета Bitwise , поэтому для их использования вам необходимо использовать побитовый модуль. Чтобы использовать его, введите следующую команду в вашей оболочке —
use Bitwise
Предположим, что A равно 5, а B равно 6 для следующих примеров:
оператор | Описание | пример |
---|---|---|
&&& | Побитовое и оператор копирует немного, чтобы привести, если это существует в обоих операндах. | A &&& B даст 4 |
||| | Побитовый или оператор копирует бит, чтобы получить результат, если он существует в любом из операндов. | A ||| Б даст 7 |
>>> | Побитовый оператор сдвига вправо сдвигает первый бит операнда вправо на число, указанное во втором операнде. | A >>> B даст 0 |
<<< | Побитовый оператор сдвига влево сдвигает первые биты операнда влево на число, указанное во втором операнде. | А <<< Б даст 320 |
^^^ | Побитовый оператор XOR копирует бит, чтобы получить результат, только если он отличается для обоих операндов. | А ^^^ Б даст 3 |
~~~ | Унарное поразрядное не инвертирует биты на заданное число. | ~~~ А даст -6 |
Разные Операторы
Помимо вышеперечисленных операторов, Elixir также предоставляет ряд других операторов, таких как оператор конкатенации, оператор совпадения, оператор вывода, оператор конвейера, оператор сопоставления строки, оператор кодовой точки, оператор захвата, троичный оператор, которые делают его довольно мощным языком.
Эликсир — образец соответствия
Сопоставление с образцом — это техника, которую Эликсир наследует от Эрланга. Это очень мощный метод, который позволяет нам извлекать более простые подструктуры из сложных структур данных, таких как списки, кортежи, карты и т. Д.
Матч состоит из двух основных частей: левой и правой . Правая сторона — это структура данных любого типа. Левая сторона пытается сопоставить структуру данных с правой стороны и связать любые переменные слева с соответствующей подструктурой справа. Если совпадение не найдено, оператор выдает ошибку.
Самое простое совпадение — это одиночная переменная слева и любая структура данных справа. Эта переменная будет соответствовать чему угодно . Например,
x = 12 x = "Hello" IO.puts(x)
Вы можете разместить переменные внутри структуры, чтобы захватить субструктуру. Например,
[var_1, _unused_var, var_2] = [{"First variable"}, 25, "Second variable" ] IO.puts(var_1) IO.puts(var_2)
Это сохранит значения {«Первая переменная»} в var_1 и «Вторая переменная» в var_2 . Существует также специальная переменная _ (или переменные с префиксом ‘_’), которая работает точно так же, как и другие переменные, но говорит elixir: «Убедитесь, что что-то здесь, но мне все равно, что это такое». , В предыдущем примере _unused_var была одной из таких переменных.
Мы можем сопоставить более сложные образцы, используя эту технику. Например, если вы хотите развернуть и получить номер в кортеже, который находится внутри списка, который сам находится в списке, вы можете использовать следующую команду —
[_, [_, {a}]] = ["Random string", [:an_atom, {24}]] IO.puts(a)
Вышеуказанная программа генерирует следующий результат —
24
Это свяжет a с 24. Другие значения игнорируются, так как мы используем ‘_’.
В сопоставлении с образцом, если мы используем переменную справа , используется ее значение. Если вы хотите использовать значение переменной слева, вам нужно использовать оператор pin.
Например, если у вас есть переменная «a», имеющая значение 25, и вы хотите сопоставить ее с другой переменной «b», имеющей значение 25, то вам нужно ввести —
a = 25 b = 25 ^a = b
Последняя строка соответствует текущему значению a , а не присваивает его значению b . Если у нас есть несоответствующий набор левой и правой стороны, оператор соответствия вызывает ошибку. Например, если мы попытаемся сопоставить кортеж со списком или список размером 2 со списком размера 3, появится сообщение об ошибке.
Эликсир — Принятие решений
Структуры принятия решений требуют, чтобы программист определял одно или несколько условий, которые должны быть оценены или протестированы программой, вместе с оператором или инструкциями, которые должны быть выполнены, если условие определено как истинное , и, необязательно, другие операторы, которые должны быть выполнены, если условие определяется как ложный .
Ниже приводится общее описание типичной структуры принятия решений, встречающейся в большинстве языков программирования.
Elixir предоставляет условные конструкции if / else, как и многие другие языки программирования. Он также имеет оператор cond, который вызывает первое найденное истинное значение. Case — это еще один оператор потока управления, который использует сопоставление с образцом для управления потоком программы. Давайте глубоко посмотрим на них.
Эликсир предоставляет следующие виды заявлений о принятии решений. Нажмите на следующие ссылки, чтобы проверить их детали.
Sr.No. | Заявление и описание |
---|---|
1 | если заявление
Оператор if состоит из логического выражения, за которым следует do , один или несколько исполняемых операторов и, наконец, ключевое слово end . Код в операторе if выполняется только в том случае, если логическое условие имеет значение true. |
2 | если .. еще заявление
За оператором if может следовать необязательный оператор else (внутри блока do..end), который выполняется, когда логическое выражение имеет значение false. |
3 | если только заявление
Оператор исключением имеет то же тело, что и оператор if. Код внутри оператора if выполняется только в том случае, если задано условие false. |
4 | если .. еще заявление
Оператор Тогда не существует. Он имеет то же тело, что и оператор if..else. Код внутри оператора if выполняется только в том случае, если задано условие false. |
5 | конд
Оператор cond используется там, где мы хотим выполнить код на основе нескольких условий. Это работает как конструкция if … else if … .else на нескольких других языках программирования. |
6 | дело
Оператор case может рассматриваться как замена оператора switch в императивных языках. Case принимает переменную / литерал и применяет сопоставление с шаблоном для разных случаев. Если какой-либо случай совпадает, Elixir выполняет код, связанный с этим случаем, и выходит из оператора case. |
Оператор if состоит из логического выражения, за которым следует do , один или несколько исполняемых операторов и, наконец, ключевое слово end . Код в операторе if выполняется только в том случае, если логическое условие имеет значение true.
За оператором if может следовать необязательный оператор else (внутри блока do..end), который выполняется, когда логическое выражение имеет значение false.
Оператор исключением имеет то же тело, что и оператор if. Код внутри оператора if выполняется только в том случае, если задано условие false.
Оператор Тогда не существует. Он имеет то же тело, что и оператор if..else. Код внутри оператора if выполняется только в том случае, если задано условие false.
Оператор cond используется там, где мы хотим выполнить код на основе нескольких условий. Это работает как конструкция if … else if … .else на нескольких других языках программирования.
Оператор case может рассматриваться как замена оператора switch в императивных языках. Case принимает переменную / литерал и применяет сопоставление с шаблоном для разных случаев. Если какой-либо случай совпадает, Elixir выполняет код, связанный с этим случаем, и выходит из оператора case.
Эликсир — Струны
Строки в Elixir вставляются между двойными кавычками и кодируются в UTF-8. В отличие от C и C ++, где строки по умолчанию кодируются в ASCII и возможны только 256 различных символов, UTF-8 состоит из 66536 кодовых точек . Это означает, что кодировка UTF-8 состоит из множества возможных символов. Поскольку в строках используется utf-8, мы также можем использовать такие символы, как: ö, ł и т. Д.
Создать строку
Чтобы создать строковую переменную, просто назначьте строку переменной —
str = "Hello world"
Чтобы распечатать это на вашей консоли, просто вызовите функцию IO.puts и передайте ей переменную str —
str = str = "Hello world" IO.puts(str)
Вышеуказанная программа генерирует следующий результат —
Hello World
Пустые строки
Вы можете создать пустую строку, используя строковый литерал «» . Например,
a = "" if String.length(a) === 0 do IO.puts("a is an empty string") end
Вышеуказанная программа генерирует следующий результат.
a is an empty string
Строковая интерполяция
Строковая интерполяция — это способ создания нового значения String из набора констант, переменных, литералов и выражений путем включения их значений в строковый литерал. Elixir поддерживает интерполяцию строк, чтобы использовать переменную в строке при ее написании, заключить ее в фигурные скобки и добавить к фигурным скобкам знак «#» .
Например,
x = "Apocalypse" y = "X-men #{x}" IO.puts(y)
Это возьмет значение x и подставит его в y. Приведенный выше код сгенерирует следующий результат —
X-men Apocalypse
Конкатенация строк
Мы уже видели использование конкатенации строк в предыдущих главах. Оператор <> используется для объединения строк в Elixir. Объединить 2 строки,
x = "Dark" y = "Knight" z = x <> " " <> y IO.puts(z)
Приведенный выше код генерирует следующий результат —
Dark Knight
Длина строки
Чтобы получить длину строки, мы используем функцию String.length . Передайте строку в качестве параметра, и он покажет вам ее размер. Например,
IO.puts(String.length("Hello"))
При запуске над программой она выдает следующий результат —
5
Сторнирование строки
Чтобы перевернуть строку, передайте ее функции String.reverse. Например,
IO.puts(String.reverse("Elixir"))
Вышеуказанная программа генерирует следующий результат —
rixilE
Сравнение строк
Чтобы сравнить 2 строки, мы можем использовать операторы == или ===. Например,
var_1 = "Hello world" var_2 = "Hello Elixir" if var_1 === var_2 do IO.puts("#{var_1} and #{var_2} are the same") else IO.puts("#{var_1} and #{var_2} are not the same") end
Вышеуказанная программа генерирует следующий результат —
Hello world and Hello elixir are not the same.
Соответствие строки
Мы уже видели использование оператора сравнения строк = ~. Чтобы проверить, соответствует ли строка регулярному выражению, мы также можем использовать оператор совпадения строк или String.match? функция. Например,
IO.puts(String.match?("foo", ~r/foo/)) IO.puts(String.match?("bar", ~r/foo/))
Вышеуказанная программа генерирует следующий результат —
true false
Это также может быть достигнуто с помощью оператора = ~. Например,
IO.puts("foo" =~ ~r/foo/)
Вышеуказанная программа генерирует следующий результат —
true
Строковые функции
Elixir поддерживает большое количество функций, связанных со строками, некоторые из наиболее часто используемых перечислены в следующей таблице.
Sr.No. | Функция и ее назначение |
---|---|
1 |
в (строка, позиция) Возвращает графему в позиции заданной строки utf8. Если позиция больше длины строки, он возвращает ноль |
2 |
капитализировать (строка) Преобразует первый символ в данной строке в верхний регистр, а остаток в нижний регистр |
3 |
содержит? (строка, содержимое) Проверяет, содержит ли строка какое-либо из заданного содержимого |
4 |
downcase (строка) Преобразует все символы в данной строке в нижний регистр |
5 |
end_with? (строка, суффиксы) Возвращает true, если строка заканчивается любым из указанных суффиксов |
6 |
первая (строка) Возвращает первый графем из строки utf8, ноль, если строка пуста |
7 |
последняя (строка) Возвращает последнюю графему из строки utf8, ноль, если строка пуста |
8 |
заменить (предмет, шаблон, замена, параметры \\ []) Возвращает новую строку, созданную путем замены вхождений шаблона в теме с заменой |
9 |
ломтик (строка, начало, длина) Возвращает подстроку, начинающуюся в начале смещения, и длины len |
10 |
Раскол (строка) Делит строку на подстроки при каждом появлении пробелов в Юникоде, игнорируя начальные и конечные пробелы. Группы пробелов рассматриваются как единое вхождение. Деления не происходят на неразрывных пробелах |
11 |
Upcase (строка) Преобразует все символы в данной строке в верхний регистр |
в (строка, позиция)
Возвращает графему в позиции заданной строки utf8. Если позиция больше длины строки, он возвращает ноль
капитализировать (строка)
Преобразует первый символ в данной строке в верхний регистр, а остаток в нижний регистр
содержит? (строка, содержимое)
Проверяет, содержит ли строка какое-либо из заданного содержимого
downcase (строка)
Преобразует все символы в данной строке в нижний регистр
end_with? (строка, суффиксы)
Возвращает true, если строка заканчивается любым из указанных суффиксов
первая (строка)
Возвращает первый графем из строки utf8, ноль, если строка пуста
последняя (строка)
Возвращает последнюю графему из строки utf8, ноль, если строка пуста
заменить (предмет, шаблон, замена, параметры \\ [])
Возвращает новую строку, созданную путем замены вхождений шаблона в теме с заменой
ломтик (строка, начало, длина)
Возвращает подстроку, начинающуюся в начале смещения, и длины len
Раскол (строка)
Делит строку на подстроки при каждом появлении пробелов в Юникоде, игнорируя начальные и конечные пробелы. Группы пробелов рассматриваются как единое вхождение. Деления не происходят на неразрывных пробелах
Upcase (строка)
Преобразует все символы в данной строке в верхний регистр
Бинарные
Двоичный файл — это просто последовательность байтов. Двоичные файлы определяются с помощью << >> . Например:
<< 0, 1, 2, 3 >>
Конечно, эти байты могут быть организованы любым способом, даже в последовательности, которая не делает их допустимой строкой. Например,
<< 239, 191, 191 >>
Строки также являются двоичными файлами. А оператор конкатенации строк <> на самом деле является оператором двоичной конкатенации:
IO.puts(<< 0, 1 >> <> << 2, 3 >>)
Приведенный выше код генерирует следующий результат —
<< 0, 1, 2, 3 >>
Обратите внимание на символ ł. Поскольку это кодируется utf-8, это символьное представление занимает 2 байта.
Поскольку каждое число, представленное в двоичном файле, должно быть байтом, когда это значение увеличивается с 255, оно усекается. Чтобы предотвратить это, мы используем модификатор размера, чтобы указать, сколько бит мы хотим, чтобы это число заняло. Например —
IO.puts(<< 256 >>) # truncated, it'll print << 0 >> IO.puts(<< 256 :: size(16) >>) #Takes 16 bits/2 bytes, will print << 1, 0 >>
Вышеуказанная программа сгенерирует следующий результат —
<< 0 >> << 1, 0 >>
Мы также можем использовать модификатор utf8, если символ является точкой кода, он будет получен в выводе; иначе байты —
IO.puts(<< 256 :: utf8 >>)
Вышеуказанная программа генерирует следующий результат —
Ā
У нас также есть функция is_binary, которая проверяет, является ли данная переменная двоичной. Обратите внимание, что только переменные, которые хранятся как кратные 8 бит, являются двоичными файлами.
Bitstrings
Если мы определим двоичный файл с использованием модификатора размера и передадим ему значение, не кратное 8, мы получим битовую строку вместо двоичного. Например,
bs = << 1 :: size(1) >> IO.puts(bs) IO.puts(is_binary(bs)) IO.puts(is_bitstring(bs))
Вышеуказанная программа генерирует следующий результат —
<< 1::size(1) >> false true
Это означает, что переменная bs не двоичная, а скорее цепочка битов. Можно также сказать, что двоичный файл — это цепочка битов, в которой число битов делится на 8. Сопоставление с образцом работает с двоичными файлами и с битовыми строками одинаково.
Эликсир — списки символов
Список символов — это не что иное, как список символов. Рассмотрим следующую программу, чтобы понять то же самое.
IO.puts('Hello') IO.puts(is_list('Hello'))
Вышеуказанная программа генерирует следующий результат —
Hello true
Вместо того, чтобы содержать байты, список символов содержит кодовые точки символов между одинарными кавычками. Таким образом, в то время как двойные кавычки представляют строку (т. Е. Двоичную), одинарные кавычки представляют список символов (т. Е. Список) . Обратите внимание, что IEx будет генерировать только кодовые точки в качестве вывода, если какой-либо из символов находится за пределами диапазона ASCII.
Списки символов используются главным образом при взаимодействии с Erlang, особенно в старых библиотеках, которые не принимают двоичные файлы в качестве аргументов. Вы можете преобразовать список символов в строку и обратно, используя функции to_string (char_list) и to_char_list (string) —
IO.puts(is_list(to_char_list("hełło"))) IO.puts(is_binary(to_string ('hełło')))
Вышеуказанная программа генерирует следующий результат —
true true
ПРИМЕЧАНИЕ. — Функции to_string и to_char_list являются полиморфными, то есть они могут принимать различные типы входных данных, такие как атомы, целые числа, и преобразовывать их в строки и списки символов соответственно.
Эликсир — списки и кортежи
(Связанные) списки
Связанный список — это гетерогенный список элементов, которые хранятся в разных местах в памяти и отслеживаются с помощью ссылок. Связанные списки — это структуры данных, особенно используемые в функциональном программировании.
Elixir использует квадратные скобки для указания списка значений. Значения могут быть любого типа —
[1, 2, true, 3]
Когда Elixir видит список печатных чисел ASCII, Elixir распечатывает их как список символов (буквально список символов). Всякий раз, когда вы видите значение в IEx и не уверены, что это такое, вы можете использовать функцию i для получения информации о нем.
IO.puts([104, 101, 108, 108, 111])
Все вышеперечисленные символы в списке доступны для печати. Когда вышеуказанная программа запущена, она дает следующий результат —
hello
Вы также можете определить списки наоборот, используя одинарные кавычки —
IO.puts(is_list('Hello'))
Когда вышеуказанная программа запущена, она дает следующий результат —
true
Имейте в виду, что одинарные и двойные кавычки не эквивалентны в Elixir, так как они представлены разными типами.
Длина списка
Чтобы найти длину списка, мы используем функцию длины, как в следующей программе:
IO.puts(length([1, 2, :true, "str"]))
Вышеуказанная программа генерирует следующий результат —
4
Конкатенация и вычитание
Два списка можно объединять и вычитать с помощью операторов ++ и — . Рассмотрим следующий пример, чтобы понять функции.
IO.puts([1, 2, 3] ++ [4, 5, 6]) IO.puts([1, true, 2, false, 3, true] -- [true, false])
Это даст вам сцепленную строку в первом случае и вычтенную строку во втором. Вышеуказанная программа генерирует следующий результат —
[1, 2, 3, 4, 5, 6] [1, 2, 3, true]
Голова и хвост списка
Голова является первым элементом списка, а хвост — остальной частью списка. Их можно получить с помощью функций hd и tl . Давайте присвоим список переменной и получим ее голову и хвост.
list = [1, 2, 3] IO.puts(hd(list)) IO.puts(tl(list))
Это даст нам заголовок и конец списка в качестве вывода. Вышеуказанная программа генерирует следующий результат —
1 [2, 3]
Примечание. Получение заголовка или хвоста пустого списка является ошибкой.
Другие функции списка
Стандартная библиотека Elixir предоставляет множество функций для работы со списками. Мы посмотрим на некоторые из них здесь. Вы можете проверить остальные здесь список .
S.no. | Название и описание функции |
---|---|
1 |
удалить (список, элемент) Удаляет данный элемент из списка. Возвращает список без элемента. Если элемент встречается в списке более одного раза, удаляется только первое вхождение. |
2 |
delete_at (список, индекс) Создает новый список, удаляя значение по указанному индексу. Отрицательные индексы указывают на смещение от конца списка. Если индекс выходит за пределы, возвращается исходный список. |
3 |
первый (список) Возвращает первый элемент в списке или ноль, если список пуст. |
4 |
расплющить (список) Сглаживает данный список вложенных списков. |
5 |
insert_at (список, индекс, значение) Возвращает список со значением, вставленным по указанному индексу. Обратите внимание, что индекс ограничен длиной списка. Отрицательные индексы указывают на смещение от конца списка. |
6 |
последний (список) Возвращает последний элемент в списке или ноль, если список пуст. |
удалить (список, элемент)
Удаляет данный элемент из списка. Возвращает список без элемента. Если элемент встречается в списке более одного раза, удаляется только первое вхождение.
delete_at (список, индекс)
Создает новый список, удаляя значение по указанному индексу. Отрицательные индексы указывают на смещение от конца списка. Если индекс выходит за пределы, возвращается исходный список.
первый (список)
Возвращает первый элемент в списке или ноль, если список пуст.
расплющить (список)
Сглаживает данный список вложенных списков.
insert_at (список, индекс, значение)
Возвращает список со значением, вставленным по указанному индексу. Обратите внимание, что индекс ограничен длиной списка. Отрицательные индексы указывают на смещение от конца списка.
последний (список)
Возвращает последний элемент в списке или ноль, если список пуст.
Кортеж
Кортежи также являются структурами данных, в которых хранится ряд других структур. В отличие от списков, они хранят элементы в непрерывном блоке памяти. Это означает, что доступ к элементу кортежа по индексу или получение размера кортежа — быстрая операция. Индексы начинаются с нуля.
Elixir использует фигурные скобки для определения кортежей. Как и списки, кортежи могут содержать любое значение —
{:ok, "hello"}
Длина кортежа
Чтобы получить длину кортежа, используйте функцию tuple_size, как в следующей программе:
IO.puts(tuple_size({:ok, "hello"}))
Вышеуказанная программа генерирует следующий результат —
2
Добавление значения
Чтобы добавить значение в кортеж, используйте функцию Tuple.append —
tuple = {:ok, "Hello"} Tuple.append(tuple, :world)
Это создаст и вернет новый кортеж: {: ok, «Hello»,: world}
Вставка значения
Чтобы вставить значение в заданную позицию, мы можем использовать либо функцию Tuple.insert_at, либо функцию put_elem . Рассмотрим следующий пример, чтобы понять то же самое —
tuple = {:bar, :baz} new_tuple_1 = Tuple.insert_at(tuple, 0, :foo) new_tuple_2 = put_elem(tuple, 1, :foobar)
Обратите внимание, что put_elem и insert_at возвращают новые кортежи. Исходный кортеж, хранящийся в переменной кортежа, не был изменен, поскольку типы данных Elixir являются неизменяемыми. Будучи неизменным, код Elixir легче рассуждать, так как вам не нужно беспокоиться о том, что конкретный код изменяет вашу структуру данных.
Кортежи и списки
В чем разница между списками и кортежами?
Списки хранятся в памяти как связанные списки, это означает, что каждый элемент в списке содержит свое значение и указывает на следующий элемент, пока не будет достигнут конец списка. Мы называем каждую пару значений и указываем на ячейку cons. Это означает, что доступ к длине списка является линейной операцией: нам нужно просмотреть весь список, чтобы выяснить его размер. Обновление списка происходит быстро, пока мы добавляем элементы.
Кортежи, с другой стороны, хранятся в памяти непрерывно. Это означает, что получение размера кортежа или доступ к элементу по индексу происходит быстро. Однако обновление или добавление элементов в кортежи стоит дорого, поскольку требует копирования всего кортежа в памяти.
Эликсир — Списки ключевых слов
До сих пор мы не обсуждали никаких ассоциативных структур данных, т. Е. Структур данных, которые могут связывать определенное значение (или несколько значений) с ключом. Разные языки называют эти функции разными именами, например словари, хэши, ассоциативные массивы и т. Д.
В Elixir у нас есть две основные ассоциативные структуры данных: списки ключевых слов и карты. В этой главе мы сосредоточимся на списках ключевых слов.
Во многих функциональных языках программирования обычно используется список из двух элементов в качестве представления ассоциативной структуры данных. В Elixir, когда у нас есть список кортежей, а первый элемент кортежа (т.е. ключ) является атомом, мы называем его списком ключевых слов. Рассмотрим следующий пример, чтобы понять то же самое —
list = [{:a, 1}, {:b, 2}]
Elixir поддерживает специальный синтаксис для определения таких списков. Мы можем поместить двоеточие в конец каждого атома и полностью избавиться от кортежей. Например,
list_1 = [{:a, 1}, {:b, 2}] list_2 = [a: 1, b: 2] IO.puts(list_1 == list_2)
Вышеуказанная программа сгенерирует следующий результат —
true
Оба из них представляют список ключевых слов. Поскольку списки ключевых слов также являются списками, мы можем использовать все операции, которые мы использовали в списках над ними.
Чтобы получить значение, связанное с атомом в списке ключевых слов, передайте атом как [] после имени списка —
list = [a: 1, b: 2] IO.puts(list[:a])
Вышеуказанная программа генерирует следующий результат —
1
Списки ключевых слов имеют три специальные характеристики —
- Ключи должны быть атомами.
- Ключи заказываются, как указано разработчиком.
- Ключи можно давать более одного раза.
Чтобы манипулировать списками ключевых слов, Elixir предоставляет модуль Keyword . Помните, однако, что списки ключевых слов — это просто списки, и поэтому они обеспечивают те же линейные характеристики производительности, что и списки. Чем длиннее список, тем больше времени потребуется для поиска ключа, подсчета количества предметов и т. Д. По этой причине списки ключевых слов используются в Elixir в основном в качестве параметров. Если вам нужно хранить много предметов или гарантировать связывание с одним ключом с максимальным значением, используйте карты.
Доступ к ключу
Чтобы получить доступ к значениям, связанным с данным ключом, мы используем функцию Keyword.get . Возвращает первое значение, связанное с данным ключом. Чтобы получить все значения, мы используем функцию Keyword.get_values. Например —
kl = [a: 1, a: 2, b: 3] IO.puts(Keyword.get(kl, :a)) IO.puts(Keyword.get_values(kl))
Вышеуказанная программа сгенерирует следующий результат —
1 [1, 2]
Вставка ключа
Чтобы добавить новое значение, используйте Keyword.put_new . Если ключ уже существует, его значение остается неизменным —
kl = [a: 1, a: 2, b: 3] kl_new = Keyword.put_new(kl, :c, 5) IO.puts(Keyword.get(kl_new, :c))
Когда вышеуказанная программа запускается, она создает новый список ключевых слов с дополнительным ключом c и генерирует следующий результат:
5
Удаление ключа
Если вы хотите удалить все записи для ключа, используйте Keyword.delete; чтобы удалить только первую запись для ключа, используйте Keyword.delete_first .
kl = [a: 1, a: 2, b: 3, c: 0] kl = Keyword.delete_first(kl, :b) kl = Keyword.delete(kl, :a) IO.puts(Keyword.get(kl, :a)) IO.puts(Keyword.get(kl, :b)) IO.puts(Keyword.get(kl, :c))
Это удалит первый b в списке и все a в списке. Когда вышеуказанная программа запущена, она выдаст следующий результат:
0
Эликсир — Карты
Списки ключевых слов — это удобный способ обращения к содержимому, хранящемуся в списках, по ключу, но под ним Elixir все еще просматривает список. Это может подойти, если у вас есть другие планы для этого списка, требующие просмотра всего этого, но это может быть ненужным, если вы планируете использовать ключи в качестве единственного подхода к данным.
Здесь карты приходят на помощь. Всякий раз, когда вам нужно хранилище значений ключей, карты представляют собой структуру данных «go to» в Elixir.
Создание карты
Карта создается с использованием синтаксиса% {} —
map = %{:a => 1, 2 => :b}
По сравнению со списками ключевых слов мы уже можем видеть два отличия:
- Карты допускают любое значение в качестве ключа.
- Ключи карт не следуют никакому порядку.
Доступ к ключу
Чтобы получить доступ к значению, связанному с ключом, Карты используют тот же синтаксис, что и списки ключевых слов —
map = %{:a => 1, 2 => :b} IO.puts(map[:a]) IO.puts(map[2])
Когда вышеуказанная программа запускается, она генерирует следующий результат —
1 b
Вставка ключа
Чтобы вставить ключ в карту, мы используем функцию Dict.put_new, которая принимает карту, новый ключ и новое значение в качестве аргументов —
map = %{:a => 1, 2 => :b} new_map = Dict.put_new(map, :new_val, "value") IO.puts(new_map[:new_val])
Это вставит пару ключ-значение : new_val — «value» в новую карту. Когда вышеуказанная программа запускается, она генерирует следующий результат —
"value"
Обновление значения
Чтобы обновить значение, уже присутствующее на карте, вы можете использовать следующий синтаксис —
map = %{:a => 1, 2 => :b} new_map = %{ map | a: 25} IO.puts(new_map[:a])
Когда вышеуказанная программа запускается, она генерирует следующий результат —
25
Сопоставление с образцом
В отличие от списков ключевых слов, карты очень полезны при сопоставлении с образцом. Когда карта используется в шаблоне, она всегда будет соответствовать подмножеству заданного значения —
%{:a => a} = %{:a => 1, 2 => :b} IO.puts(a)
Вышеуказанная программа генерирует следующий результат —
1
Это будет соответствовать 1 . И, следовательно, он будет генерировать вывод как 1 .
Как показано выше, карта совпадает до тех пор, пока ключи в шаблоне существуют на данной карте. Следовательно, пустая карта соответствует всем картам.
Переменные могут быть использованы при доступе, сопоставлении и добавлении ключей карты —
n = 1 map = %{n => :one} %{^n => :one} = %{1 => :one, 2 => :two, 3 => :three}
Модуль Map предоставляет очень похожий API на модуль Keyword с удобными функциями для управления картами. Вы можете использовать такие функции, как Map.get, Map.delete , для управления картами.
Карты с ключами Atom
Карты имеют несколько интересных свойств. Когда все ключи на карте являются атомами, для удобства вы можете использовать синтаксис ключевого слова —
map = %{:a => 1, 2 => :b} IO.puts(map.a)
Другое интересное свойство карт заключается в том, что они предоставляют собственный синтаксис для обновления и доступа к ключам атомов —
map = %{:a => 1, 2 => :b} IO.puts(map.a)
Вышеуказанная программа генерирует следующий результат —
1
Обратите внимание, что для доступа к ключам атомов таким способом он должен существовать, иначе программа не сможет работать.
Эликсир — Модули
В Elixir мы группируем несколько функций в модули. Мы уже использовали различные модули в предыдущих главах, такие как модуль String, модуль Bitwise, модуль Tuple и т. Д.
Для создания собственных модулей в Elixir мы используем макрос defmodule . Мы используем макрос def для определения функций в этом модуле —
defmodule Math do def sum(a, b) do a + b end end
В следующих разделах наши примеры будут длиннее по размеру, и будет сложно ввести их все в оболочку. Нам нужно научиться компилировать код Elixir, а также запускать скрипты Elixir.
компиляция
Модули всегда удобно записывать в файлы, чтобы их можно было скомпилировать и использовать повторно. Предположим, у нас есть файл с именем math.ex со следующим содержимым:
defmodule Math do def sum(a, b) do a + b end end
Мы можем скомпилировать файлы с помощью команды — elixirc :
$ elixirc math.ex
Это создаст файл с именем Elixir.Math.beam, содержащий байт-код для определенного модуля. Если мы снова запустим iex , наше определение модуля будет доступно (при условии, что iex запущен в том же каталоге, в котором находится файл байт-кода). Например,
IO.puts(Math.sum(1, 2))
Вышеуказанная программа сгенерирует следующий результат —
3
Скриптовый режим
В дополнение к расширению файла Elixir .ex , Elixir также поддерживает файлы .exs для сценариев. Эликсир обрабатывает оба файла одинаково, единственное отличие — в цели. Файлы .ex предназначены для компиляции, а файлы .exs используются для сценариев . При выполнении оба расширения компилируют и загружают свои модули в память, хотя только файлы .ex записывают свой байт-код на диск в формате файлов .beam.
Например, если мы хотим запустить Math.sum в том же файле, мы можем использовать .exs следующим образом —
Math.exs
defmodule Math do def sum(a, b) do a + b end end IO.puts(Math.sum(1, 2))
Мы можем запустить его с помощью команды Elixir —
$ elixir math.exs
Вышеуказанная программа сгенерирует следующий результат —
3
Файл будет скомпилирован в памяти и выполнен с выводом «3». Файл байт-кода не будет создан.
Вложение модуля
Модули могут быть вложены в эликсир. Эта особенность языка помогает нам лучше организовать наш код. Для создания вложенных модулей мы используем следующий синтаксис —
defmodule Foo do #Foo module code here defmodule Bar do #Bar module code here end end
В приведенном выше примере будут определены два модуля: Foo и Foo.Bar . Второй может быть доступен как Bar внутри Foo, если они находятся в той же лексической области. Если позже модуль Bar перемещается за пределы определения модуля Foo, на него должно ссылаться его полное имя (Foo.Bar), или псевдоним должен быть установлен с использованием директивы псевдонима, описанной в главе о псевдонимах.
Примечание. В Elixir нет необходимости определять модуль Foo для определения модуля Foo.Bar, поскольку язык переводит все имена модулей в атомы. Вы можете определить произвольно заданные модули без определения какого-либо модуля в цепочке. Например, вы можете определить Foo.Bar.Baz без определения Foo или Foo.Bar .
Эликсир — псевдонимы
Чтобы упростить повторное использование программного обеспечения, Elixir предоставляет три директивы — псевдоним, require и import . Он также предоставляет макрос с именем use, который кратко изложен ниже —
# Alias the module so it can be called as Bar instead of Foo.Bar alias Foo.Bar, as: Bar # Ensure the module is compiled and available (usually for macros) require Foo # Import functions from Foo so they can be called without the `Foo.` prefix import Foo # Invokes the custom code defined in Foo as an extension point use Foo
Давайте теперь разберемся подробно в каждой директиве.
псевдоним
Директива псевдонимов позволяет вам устанавливать псевдонимы для любого данного имени модуля. Например, если вы хотите дать псевдоним ‘Str’ модулю String, вы можете просто написать —
alias String, as: Str IO.puts(Str.length("Hello"))
Вышеуказанная программа генерирует следующий результат —
5
Псевдоним дается модулю String как Str . Теперь, когда мы вызываем любую функцию, используя литерал Str, она фактически ссылается на модуль String . Это очень полезно, когда мы используем очень длинные имена модулей и хотим заменить их на более короткие в текущей области.
ПРИМЕЧАНИЕ. — Псевдонимы ДОЛЖНЫ начинаться с заглавной буквы.
Псевдонимы действительны только в той лексической области, в которой они вызываются. Например, если у вас есть 2 модуля в файле и вы создали псевдоним в одном из модулей, этот псевдоним не будет доступен во втором модуле.
Если вы дадите имя встроенному модулю, такому как String или Tuple, в качестве псевдонима для какого-либо другого модуля, чтобы получить доступ к встроенному модулю, вам нужно будет добавить его перед «Elixir». , Например,
alias List, as: String #Now when we use String we are actually using List. #To use the string module: IO.puts(Elixir.String.length("Hello"))
Когда вышеуказанная программа запускается, она генерирует следующий результат —
5
требовать
Elixir предоставляет макросы как механизм метапрограммирования (написания кода, генерирующего код).
Макросы — это фрагменты кода, которые выполняются и раскрываются во время компиляции. Это означает, что для использования макроса мы должны гарантировать, что его модуль и реализация доступны во время компиляции. Это делается с помощью директивы require .
Integer.is_odd(3)
Когда вышеуказанная программа запущена, она выдаст следующий результат:
** (CompileError) iex:1: you must require Integer before invoking the macro Integer.is_odd/1
В Elixir Integer.is_odd определяется как макрос . Этот макрос можно использовать как охрану. Это означает, что для вызова Integer.is_odd нам понадобится модуль Integer.
Используйте функцию Integer require и запустите программу, как показано ниже.
require Integer Integer.is_odd(3)
На этот раз программа запустится и выдаст вывод: true .
Как правило, модуль не требуется перед использованием, кроме случаев, когда мы хотим использовать макросы, доступные в этом модуле. Попытка вызова макроса, который не был загружен, вызовет ошибку. Обратите внимание, что, как и директива alias, require также имеет лексическую область . Подробнее о макросах мы поговорим в следующей главе.
Импортировать
Мы используем директиву import для легкого доступа к функциям или макросам из других модулей без использования полного имени. Например, если мы хотим использовать функцию дублирования из модуля List несколько раз, мы можем просто импортировать ее.
import List, only: [duplicate: 2]
В этом случае мы импортируем только дубликат функции (с длиной списка аргументов 2) из списка. Хотя : only является необязательным, его использование рекомендуется во избежание импорта всех функций данного модуля в пространство имен. : Кроме того, можно также указать в качестве опции, чтобы импортировать все в модуле, кроме списка функций.
Директива import также поддерживает : макросы и : функции, которые должны быть переданы : only . Например, чтобы импортировать все макросы, пользователь может написать:
import Integer, only: :macros
Обратите внимание, что импорт тоже ограничен Lexical, как директивы require и alias. Также обратите внимание, что для «импорта» модуля также «требуется» .
использование
Хотя это и не директива, use — это макрос, тесно связанный с require, который позволяет вам использовать модуль в текущем контексте. Макрос использования часто используется разработчиками для внесения внешней функциональности в текущую лексическую область, часто это модули. Давайте разберемся с директивой использования на примере —
defmodule Example do use Feature, option: :value end
Use — это макрос, который преобразует вышеперечисленное в —
defmodule Example do require Feature Feature.__using__(option: :value) end
Для использования модуля сначала требуется модуль, а затем вызывается макрос __using__ для модуля. Elixir обладает большими возможностями метапрограммирования и имеет макросы для генерации кода во время компиляции. Макрос _ _using__ вызывается в приведенном выше примере, и код внедряется в наш локальный контекст. Локальный контекст — это то, где макрос использования был вызван во время компиляции.
Эликсир — Функции
Функция — это набор операторов, организованных вместе для выполнения определенной задачи. Функции в программировании работают в основном как функции в математике. Вы даете функциям некоторый ввод, они генерируют вывод на основе предоставленного ввода.
В эликсире есть 2 типа функций —
Анонимная функция
Функции, определенные с использованием конструкции fn..end, являются анонимными функциями. Эти функции иногда также называют лямбдами. Они используются путем присвоения им имен переменных.
Именованная функция
Функции, определенные с помощью ключевого слова def , называются функциями. Это встроенные функции в Elixir.
Анонимные функции
Как следует из названия, анонимная функция не имеет имени. Они часто передаются другим функциям. Чтобы определить анонимную функцию в Elixir, нам нужны ключевые слова fn и end . Внутри них мы можем определить любое количество параметров и тел функций, разделенных -> . Например,
sum = fn (a, b) -> a + b end IO.puts(sum.(1, 5))
При запуске вышеупомянутой программы, она генерирует следующий результат —
6
Обратите внимание, что эти функции не вызываются как именованные функции. У нас есть ‘ . ‘между именем функции и ее аргументами.
Использование оператора захвата
Мы также можем определить эти функции, используя оператор захвата. Это более простой способ создания функций. Теперь мы определим вышеуказанную функцию суммы с помощью оператора захвата,
sum = &(&1 + &2) IO.puts(sum.(1, 2))
Когда вышеуказанная программа запускается, она генерирует следующий результат —
3
В сокращенной версии наши параметры не названы, но доступны для нас как & 1, & 2, & 3 и так далее.
Функции сопоставления с образцом
Сопоставление с образцом не ограничивается только переменными и структурами данных. Мы можем использовать сопоставление с образцом, чтобы сделать наши функции полиморфными. Например, мы объявим функцию, которая может принимать 1 или 2 входа (в кортеже) и выводить их на консоль,
handle_result = fn {var1} -> IO.puts("#{var1} found in a tuple!") {var_2, var_3} -> IO.puts("#{var_2} and #{var_3} found!") end handle_result.({"Hey people"}) handle_result.({"Hello", "World"})
Когда вышеуказанная программа запущена, она дает следующий результат —
Hey people found in a tuple! Hello and World found!
Именованные функции
Мы можем определять функции с именами, чтобы мы могли легко ссылаться на них позже. Именованные функции определяются внутри модуля с помощью ключевого слова def. Именованные функции всегда определяются в модуле. Для вызова именованных функций нам нужно ссылаться на них, используя их имя модуля.
Ниже приведен синтаксис для именованных функций —
def function_name(argument_1, argument_2) do #code to be executed when function is called end
Давайте теперь определим нашу именованную функцию sum в модуле Math.
defmodule Math do def sum(a, b) do a + b end end IO.puts(Math.sum(5, 6))
При запуске над программой она выдает следующий результат —
11
Для функций с 1 строкой существует сокращенная запись, чтобы определить эти функции, используя do:. Например —
defmodule Math do def sum(a, b), do: a + b end IO.puts(Math.sum(5, 6))
При запуске над программой она выдает следующий результат —
11
Частные функции
Elixir предоставляет нам возможность определять частные функции, к которым можно получить доступ из модуля, в котором они определены. Чтобы определить приватную функцию, используйте defp вместо def . Например,
defmodule Greeter do def hello(name), do: phrase <> name defp phrase, do: "Hello " end Greeter.hello("world")
Когда вышеуказанная программа запущена, она дает следующий результат —
Hello world
Но если мы просто попытаемся явно вызвать функцию фразы, используя функцию Greeter.phrase () , это вызовет ошибку.
Аргументы по умолчанию
Если нам нужно значение по умолчанию для аргумента, мы используем синтаксис аргумента \\ value —
defmodule Greeter do def hello(name, country \\ "en") do phrase(country) <> name end defp phrase("en"), do: "Hello, " defp phrase("es"), do: "Hola, " end Greeter.hello("Ayush", "en") Greeter.hello("Ayush") Greeter.hello("Ayush", "es")
Когда вышеуказанная программа запущена, она дает следующий результат —
Hello, Ayush Hello, Ayush Hola, Ayush
Эликсир — Рекурсия
Рекурсия — это метод, в котором решение проблемы зависит от решения меньших экземпляров той же проблемы. Большинство языков программирования поддерживают рекурсию, позволяя функции вызывать себя внутри текста программы.
В идеале рекурсивные функции имеют конечное условие. Это конечное условие, также известное как базовый случай, прекращает повторный ввод функции и добавление вызовов функций в стек. На этом рекурсивный вызов функции останавливается. Давайте рассмотрим следующий пример, чтобы лучше понять рекурсивную функцию.
defmodule Math do def fact(res, num) do if num === 1 do res else new_res = res * num fact(new_res, num-1) end end end IO.puts(Math.fact(1,5))
Когда вышеуказанная программа запускается, она генерирует следующий результат —
120
Таким образом, в приведенной выше функции, Math.fact , мы вычисляем факториал числа. Обратите внимание, что мы вызываем функцию внутри себя. Давайте теперь поймем, как это работает.
Мы предоставили ему 1 и число, чей факториал мы хотим вычислить. Функция проверяет, является ли число 1 или нет, и возвращает res, если оно равно 1 (Условие завершения) . Если нет, то он создает переменную new_res и присваивает ей значение предыдущего res * current num. Он возвращает значение, возвращенное фактом вызова нашей функции (new_res, num-1) . Это повторяется до тех пор, пока мы не получим num как 1. Как только это произойдет, мы получим результат.
Давайте рассмотрим другой пример, печатая каждый элемент списка по одному. Для этого мы будем использовать функции hd и tl списков и сопоставления с образцом в функциях —
a = ["Hey", 100, 452, :true, "People"] defmodule ListPrint do def print([]) do end def print([head | tail]) do IO.puts(head) print(tail) end end ListPrint.print(a)
Первая функция печати вызывается, когда у нас есть пустой список (конечное условие) . Если нет, то будет вызвана вторая функция печати, которая разделит список на 2 и назначит первый элемент списка заголовку, а оставшийся список — хвосту. Затем печатается голова, и мы снова вызываем функцию печати с остальной частью списка, т. Е. С хвостом. Когда вышеуказанная программа запущена, она дает следующий результат —
Hey 100 452 true People
Эликсир — Петли
Из-за неизменности циклы в Elixir (как и в любом функциональном языке программирования) пишутся не так, как императивные языки. Например, на императивном языке, таком как C, вы напишите:
for(i = 0; i < 10; i++) { printf("%d", array[i]); }
В приведенном выше примере мы мутируем как массив, так и переменную i . Мутация невозможна в эликсире. Вместо этого функциональные языки полагаются на рекурсию: функция вызывается рекурсивно, пока не будет достигнуто условие, которое останавливает продолжение рекурсивного действия. В этом процессе данные не видоизменяются.
Давайте теперь напишем простой цикл с использованием рекурсии, который печатает привет n раз.
defmodule Loop do def print_multiple_times(msg, n) when n <= 1 do IO.puts msg end def print_multiple_times(msg, n) do IO.puts msg print_multiple_times(msg, n - 1) end end Loop.print_multiple_times("Hello", 10)
Когда вышеуказанная программа запущена, она дает следующий результат —
Hello Hello Hello Hello Hello Hello Hello Hello Hello Hello
Мы использовали методы сопоставления с образцом функции и рекурсию, чтобы успешно реализовать цикл. Рекурсивные определения трудно понять, но преобразование циклов в рекурсию легко.
Эликсир предоставляет нам модуль Enum . Этот модуль используется для большинства итеративных циклических вызовов, поскольку их гораздо проще использовать, чем пытаться определить рекурсивные определения для них. Мы обсудим это в следующей главе. Ваши собственные рекурсивные определения следует использовать только тогда, когда вы не можете найти решение с использованием этого модуля. Эти функции оптимизированы для быстрого вызова.
Эликсир — перечислимые
Перечислимый — это объект, который может быть перечислен. «Перечислимый» означает подсчитывать членов набора / коллекции / категории один за другим (обычно по порядку, обычно по имени).
Elixir предлагает концепцию перечислимых элементов и модуль Enum для работы с ними. Функции в модуле Enum ограничены, как следует из названия, перечислением значений в структурах данных. Примером перечислимой структуры данных является список, кортеж, карта и т. Д. Модуль Enum предоставляет нам чуть более 100 функций для работы с перечислениями. Мы обсудим несколько важных функций в этой главе.
Все эти функции принимают перечислимый в качестве первого элемента и функцию в качестве второго и работают с ними. Функции описаны ниже.
все?
Когда мы используем все ? функция, вся коллекция должна иметь значение true, в противном случае будет возвращено значение false. Например, чтобы проверить, являются ли все элементы в списке нечетными числами, тогда.
res = Enum.all?([1, 2, 3, 4], fn(s) -> rem(s,2) == 1 end) IO.puts(res)
Когда вышеуказанная программа запущена, она дает следующий результат —
false
Это потому, что не все элементы этого списка нечетны.
любой?
Как следует из названия, эта функция возвращает true, если какой-либо элемент коллекции оценивается как true. Например —
res = Enum.any?([1, 2, 3, 4], fn(s) -> rem(s,2) == 1 end) IO.puts(res)
Когда вышеуказанная программа запущена, она дает следующий результат —
true
ломоть
Эта функция делит нашу коллекцию на маленькие порции размера, указанного в качестве второго аргумента. Например —
res = Enum.chunk([1, 2, 3, 4, 5, 6], 2) IO.puts(res)
Когда вышеуказанная программа запущена, она дает следующий результат —
[[1, 2], [3, 4], [5, 6]]
каждый
Может потребоваться перебрать коллекцию без создания нового значения, для этого случая мы используем каждую функцию —
Enum.each(["Hello", "Every", "one"], fn(s) -> IO.puts(s) end)
Когда вышеуказанная программа запущена, она дает следующий результат —
Hello Every one
карта
Чтобы применить нашу функцию к каждому элементу и создать новую коллекцию, мы используем функцию карты. Это одна из самых полезных конструкций в функциональном программировании, так как она довольно выразительна и коротка. Давайте рассмотрим пример, чтобы понять это. Мы удвоим значения, хранящиеся в списке, и сохраним их в новом списке —
res = Enum.map([2, 5, 3, 6], fn(a) -> a*2 end) IO.puts(res)
Когда вышеуказанная программа запущена, она дает следующий результат —
[4, 10, 6, 12]
уменьшить
Функция Reduce помогает нам уменьшить наше перечисляемое значение до единственного значения. Для этого мы поставляем дополнительный аккумулятор (в нашем примере 5) для передачи в нашу функцию; если аккумулятор не указан, используется первое значение —
res = Enum.reduce([1, 2, 3, 4], 5, fn(x, accum) -> x + accum end) IO.puts(res)
Когда вышеуказанная программа запущена, она дает следующий результат —
15
Аккумулятор — это начальное значение, передаваемое в fn . Начиная со второго вызова, значение, возвращаемое предыдущим вызовом, передается как накопленный. Мы также можем использовать уменьшить без аккумулятора —
res = Enum.reduce([1, 2, 3, 4], fn(x, accum) -> x + accum end) IO.puts(res)
Когда вышеуказанная программа запущена, она дает следующий результат —
10
уник
Функция uniq удаляет дубликаты из нашей коллекции и возвращает только набор элементов в коллекции. Например —
res = Enum.uniq([1, 2, 2, 3, 3, 3, 4, 4, 4, 4]) IO.puts(res)
При запуске вышеуказанной программы, она дает следующий результат —
[1, 2, 3, 4]
Стремительная оценка
Все функции в модуле Enum нетерпеливы. Многие функции ожидают перечисления и возвращают список обратно. Это означает, что при выполнении нескольких операций с Enum каждая операция будет генерировать промежуточный список, пока мы не достигнем результата. Давайте рассмотрим следующий пример, чтобы понять это —
odd? = &(odd? = &(rem(&1, 2) != 0) res = 1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(odd?) |> Enum.sum IO.puts(res)
Когда вышеуказанная программа запущена, она дает следующий результат —
7500000000
В приведенном выше примере есть конвейер операций. Мы начнем с диапазона, а затем умножим каждый элемент в диапазоне на 3. Эта первая операция теперь создаст и вернет список из 100_000 элементов. Затем мы сохраняем все нечетные элементы из списка, генерируя новый список, теперь с 50_000 элементами, а затем суммируем все записи.
Символ |>, используемый в приведенном выше фрагменте, является оператором канала : он просто берет вывод из выражения с левой стороны и передает его в качестве первого аргумента в вызов функции с правой стороны. Это похоже на Unix | оператор. Его цель состоит в том, чтобы выделить поток данных, преобразуемых с помощью ряда функций.
Без оператора канала код выглядит сложным —
Enum.sum(Enum.filter(Enum.map(1..100_000, &(&1 * 3)), odd?))
У нас есть много других функций, однако, только несколько важных были описаны здесь.
Эликсир — Потоки
Многие функции ожидают перечисления и возвращают список обратно. Это означает, что при выполнении нескольких операций с Enum каждая операция будет генерировать промежуточный список, пока мы не достигнем результата.
Потоки поддерживают ленивые операции, а не активные операции перечислений. Короче говоря, потоки являются ленивыми, компонуемыми перечислимыми . Это означает, что Streams не выполняет операцию, если она не является абсолютно необходимой. Давайте рассмотрим пример, чтобы понять это —
odd? = &(rem(&1, 2) != 0) res = 1..100_000 |> Stream.map(&(&1 * 3)) |> Stream.filter(odd?) |> Enum.sum IO.puts(res)
Когда вышеуказанная программа запущена, она дает следующий результат —
7500000000
В приведенном выше примере 1..100_000 |> Stream.map (& (& 1 * 3)) возвращает тип данных, фактический поток, который представляет вычисление карты в диапазоне 1..100_000. Он еще не оценил это представление. Вместо создания промежуточных списков потоки создают серию вычислений, которые вызываются только тогда, когда мы передаем основной поток в модуль Enum. Потоки полезны при работе с большими, возможно бесконечными, коллекциями.
Потоки и перечисления имеют много общих функций. Потоки в основном предоставляют те же функции, которые предоставляет модуль Enum, который генерирует списки в качестве возвращаемых значений после выполнения вычислений для входных перечислимых элементов. Некоторые из них перечислены в следующей таблице —
Sr.No. | Функция и ее описание |
---|---|
1 |
кусок (перечисление, n, шаг, остаток \\ nil) Потоки перечислимых в чанках, содержащих по n элементов каждый, где каждый новый чанк запускает пошаговые элементы в перечислимых. |
2 |
CONCAT (перечислимых) Создает поток, который перечисляет каждое перечисляемое в перечисляемом. |
3 |
каждый (перечисление, веселье) Выполняет данную функцию для каждого элемента. |
4 |
фильтр (перечисление, веселье) Создает поток, который фильтрует элементы в соответствии с заданной функцией при перечислении. |
5 |
карта (перечисление, веселье) Создает поток, который будет применять данную функцию при перечислении. |
6 |
drop (enum, n) Лениво выбрасывает следующие n предметов из перечисленного. |
кусок (перечисление, n, шаг, остаток \\ nil)
Потоки перечислимых в чанках, содержащих по n элементов каждый, где каждый новый чанк запускает пошаговые элементы в перечислимых.
CONCAT (перечислимых)
Создает поток, который перечисляет каждое перечисляемое в перечисляемом.
каждый (перечисление, веселье)
Выполняет данную функцию для каждого элемента.
фильтр (перечисление, веселье)
Создает поток, который фильтрует элементы в соответствии с заданной функцией при перечислении.
карта (перечисление, веселье)
Создает поток, который будет применять данную функцию при перечислении.
drop (enum, n)
Лениво выбрасывает следующие n предметов из перечисленного.
Эликсир — Структуры
Структуры — это расширения, построенные поверх карт, которые обеспечивают проверки во время компиляции и значения по умолчанию.
Определение структур
Для определения структуры используется конструкция defstruct —
defmodule User do defstruct name: "John", age: 27 end
Список ключевых слов, используемых с defstruct, определяет, какие поля будут иметь структура вместе со значениями по умолчанию. Структуры берут имя модуля, в котором они определены. В приведенном выше примере мы определили структуру с именем User. Теперь мы можем создавать пользовательские структуры, используя синтаксис, аналогичный тому, который используется для создания карт —
new_john = %User{}) ayush = %User{name: "Ayush", age: 20} megan = %User{name: "Megan"})
Приведенный выше код сгенерирует три различных структуры со значениями:
%User{age: 27, name: "John"} %User{age: 20, name: "Ayush"} %User{age: 27, name: "Megan"}
Структуры предоставляют гарантии времени компиляции, что только поля (и все они), определенные с помощью defstruct, могут существовать в структуре. Таким образом, вы не можете определить свои собственные поля после того, как создали структуру в модуле.
Доступ и обновление структур
Когда мы обсуждали карты, мы показали, как мы можем получить доступ и обновить поля карты. Те же методы (и тот же синтаксис) применимы и к структурам. Например, если мы хотим обновить пользователя, которого мы создали в предыдущем примере, то —
defmodule User do defstruct name: "John", age: 27 end john = %User{} #john right now is: %User{age: 27, name: "John"} #To access name and age of John, IO.puts(john.name) IO.puts(john.age)
Когда вышеуказанная программа запущена, она дает следующий результат —
John 27
Чтобы обновить значение в структуре, мы снова будем использовать ту же процедуру, что мы использовали в главе карты,
meg = %{john | name: "Meg"}
Структуры также могут использоваться при сопоставлении с образцом, как для сопоставления по значению конкретных ключей, так и для обеспечения того, чтобы сопоставляемое значение представляло собой структуру того же типа, что и сопоставляемое значение.
Эликсир — Протоколы
Протоколы являются механизмом достижения полиморфизма в эликсире. Диспетчеризация по протоколу доступна для любого типа данных, если он реализует протокол.
Давайте рассмотрим пример использования протоколов. Мы использовали функцию to_string в предыдущих главах для преобразования других типов в строковый тип. Это на самом деле протокол. Он действует в соответствии с вводимыми данными без ошибок. Может показаться, что мы обсуждаем функции сопоставления с образцом, но если мы продолжим работу, то получится иначе.
Рассмотрим следующий пример, чтобы лучше понять механизм протокола.
Давайте создадим протокол, который будет отображать, является ли данный ввод пустым или нет. Мы будем называть этот протокол пустым? ,
Определение протокола
Мы можем определить протокол в Elixir следующим образом —
defprotocol Blank do def blank?(data) end
Как видите, нам не нужно определять тело для функции. Если вы знакомы с интерфейсами в других языках программирования, вы можете думать о протоколе как о том же самом.
Таким образом, этот протокол говорит, что все, что реализует его, должно быть пустым? функции, хотя это зависит от разработчика относительно того, как функция отвечает. Определив протокол, давайте разберемся, как добавить пару реализаций.
Реализация протокола
Поскольку мы определили протокол, теперь нам нужно указать ему, как обрабатывать различные входные данные, которые он может получить. Давайте продолжим на примере, который мы взяли ранее. Мы реализуем пустой протокол для списков, карт и строк. Это покажет, пусто или нет то, что мы передали.
#Defining the protocol defprotocol Blank do def blank?(data) end #Implementing the protocol for lists defimpl Blank, for: List do def blank?([]), do: true def blank?(_), do: false end #Implementing the protocol for strings defimpl Blank, for: BitString do def blank?(""), do: true def blank?(_), do: false end #Implementing the protocol for maps defimpl Blank, for: Map do def blank?(map), do: map_size(map) == 0 end IO.puts(Blank.blank? []) IO.puts(Blank.blank? [:true, "Hello"]) IO.puts(Blank.blank? "") IO.puts(Blank.blank? "Hi")
Вы можете реализовать свой протокол для любого количества или нескольких типов, в зависимости от того, что имеет смысл для использования вашего протокола. Это был довольно простой случай использования протоколов. Когда вышеуказанная программа запущена, она дает следующий результат —
true false true false
Примечание. Если вы используете это для любых типов, отличных от тех, для которых вы определили протокол, это приведет к ошибке.
Эликсир — File IO
Файловый ввод-вывод является неотъемлемой частью любого языка программирования, поскольку он позволяет языку взаимодействовать с файлами в файловой системе. В этой главе мы обсудим два модуля — Path и File.
Модуль Path
Модуль path — это очень маленький модуль, который можно рассматривать как вспомогательный модуль для операций с файловой системой. Большинство функций в модуле File ожидают пути в качестве аргументов. Чаще всего эти пути будут обычными двоичными файлами. Модуль Path предоставляет возможности для работы с такими путями. Использование функций из модуля Path, а не просто манипулирование двоичными файлами, является предпочтительным, поскольку модуль Path прозрачно заботится о различных операционных системах. Следует отметить, что Elixir автоматически преобразует косую черту (/) в обратную косую черту (\) в Windows при выполнении файловых операций.
Давайте рассмотрим следующий пример, чтобы лучше понять модуль Path —
IO.puts(Path.join("foo", "bar"))
Когда вышеуказанная программа запущена, она дает следующий результат —
foo/bar
Есть много методов, которые предоставляет модуль path. Вы можете взглянуть на различные методы здесь . Эти методы часто используются, если вы выполняете много операций с файлами.
Файловый модуль
Файловый модуль содержит функции, которые позволяют нам открывать файлы как устройства ввода-вывода. По умолчанию файлы открываются в двоичном режиме, что требует от разработчиков использования определенных функций IO.binread и IO.binwrite из модуля IO. Давайте создадим файл с именем newfile и запишем в него некоторые данные.
{:ok, file} = File.read("newfile", [:write]) # Pattern matching to store returned stream IO.binwrite(file, "This will be written to the file")
Если вы откроете файл, в который мы только что написали, содержимое будет отображаться следующим образом:
This will be written to the file
Давайте теперь разберемся, как использовать файловый модуль.
Открытие файла
Чтобы открыть файл, мы можем использовать любую из следующих двух функций:
{:ok, file} = File.open("newfile") file = File.open!("newfile")
Давайте теперь поймем разницу между функцией File.open и функцией File.open! () .
-
Функция File.open всегда возвращает кортеж. Если файл успешно открыт, он возвращает первое значение в кортеже как : ok, а второе значение — литерал типа io_device. Если возникла ошибка, он вернет кортеж с первым значением как : error и вторым значением в качестве причины.
-
Функция File.open! (), С другой стороны, вернет io_device, если файл успешно открыт, иначе возникнет ошибка. ПРИМЕЧАНИЕ. Это шаблон, который следует всем функциям файлового модуля, которые мы собираемся обсудить.
Функция File.open всегда возвращает кортеж. Если файл успешно открыт, он возвращает первое значение в кортеже как : ok, а второе значение — литерал типа io_device. Если возникла ошибка, он вернет кортеж с первым значением как : error и вторым значением в качестве причины.
Функция File.open! (), С другой стороны, вернет io_device, если файл успешно открыт, иначе возникнет ошибка. ПРИМЕЧАНИЕ. Это шаблон, который следует всем функциям файлового модуля, которые мы собираемся обсудить.
Мы также можем указать режимы, в которых мы хотим открыть этот файл. Чтобы открыть файл только для чтения и в режиме кодирования utf-8, мы используем следующий код —
file = File.open!("newfile", [:read, :utf8])
Запись в файл
У нас есть два способа записи в файлы. Давайте посмотрим на первый, используя функцию записи из модуля File.
File.write("newfile", "Hello")
Но это не должно использоваться, если вы делаете несколько записей в один и тот же файл. Каждый раз, когда вызывается эта функция, открывается файловый дескриптор и создается новый процесс для записи в файл. Если вы делаете несколько записей в цикле, откройте файл с помощью File.open и запишите его, используя методы модуля IO. Давайте рассмотрим пример, чтобы понять то же самое —
#Open the file in read, write and utf8 modes. file = File.open!("newfile_2", [:read, :utf8, :write]) #Write to this "io_device" using standard IO functions IO.puts(file, "Random text")
Вы можете использовать другие методы модуля ввода-вывода, такие как IO.write и IO.binwrite, для записи в файлы, открытые как io_device.
Чтение из файла
У нас есть два способа чтения из файлов. Давайте посмотрим на первый, используя функцию чтения из модуля File.
IO.puts(File.read("newfile"))
При запуске этого кода вы должны получить кортеж с первым элементом как : ok, а второй — как содержимое newfile.
Мы также можем использовать File.read! функция, чтобы просто получить содержимое файлов, возвращенных нам.
Закрытие открытого файла
Всякий раз, когда вы открываете файл с помощью функции File.open, после того, как вы его закончили, вы должны закрыть его с помощью функции File.close —
File.close(file)
Эликсир — Процессы
В Elixir весь код выполняется внутри процессов. Процессы изолированы друг от друга, работают параллельно друг с другом и обмениваются сообщениями. Процессы Elixir не следует путать с процессами операционной системы. Процессы в Elixir чрезвычайно легки с точки зрения памяти и процессора (в отличие от потоков во многих других языках программирования). Из-за этого весьма обычно иметь десятки или даже сотни тысяч процессов, работающих одновременно.
В этой главе мы узнаем об основных конструкциях для порождения новых процессов, а также отправки и получения сообщений между различными процессами.
Функция появления
Самый простой способ создать новый процесс — использовать функцию spawn . Спаун принимает функцию, которая будет запущена в новом процессе. Например —
pid = spawn(fn -> 2 * 2 end) Process.alive?(pid)
Когда вышеуказанная программа запущена, она дает следующий результат —
false
Возвращаемое значение функции spawn — PID. Это уникальный идентификатор процесса, поэтому, если вы запустите код над своим PID, он будет другим. Как вы можете видеть в этом примере, процесс мертв, когда мы проверяем, жив ли он. Это потому, что процесс завершится, как только завершит выполнение данной функции.
Как уже упоминалось, все коды Elixir выполняются внутри процессов. Если вы запустите функцию self, вы увидите PID для вашего текущего сеанса —
pid = self Process.alive?(pid)
Когда вышеуказанная программа запущена, она дает следующий результат —
true
Передача сообщений
Мы можем отправлять сообщения процессу с помощью send и получать их с помощью receive . Давайте передадим сообщение текущему процессу и получим его так же.
send(self(), {:hello, "Hi people"}) receive do {:hello, msg} -> IO.puts(msg) {:another_case, msg} -> IO.puts("This one won't match!") end
Когда вышеуказанная программа запущена, она дает следующий результат —
Hi people
Мы отправили сообщение текущему процессу с помощью функции send и передали его в PID self. Затем мы обработали входящее сообщение с помощью функции receive .
Когда сообщение отправляется процессу, оно сохраняется в почтовом ящике процесса . Блок получения проходит через текущий почтовый ящик процесса в поисках сообщения, соответствующего любому из заданных шаблонов. Блок приема поддерживает охрану и многие пункты, такие как case.
Если в почтовом ящике нет сообщений, соответствующих какому-либо из шаблонов, текущий процесс будет ожидать поступления соответствующего сообщения. Время ожидания также может быть указано. Например,
receive do {:hello, msg} -> msg after 1_000 -> "nothing after 1s" end
Когда вышеуказанная программа запущена, она дает следующий результат —
nothing after 1s
ПРИМЕЧАНИЕ. — Время ожидания 0 может быть задано, когда вы уже ожидаете, что сообщение будет находиться в почтовом ящике.
связи
Самая распространенная форма появления в Elixir — это функция spawn_link . Прежде чем взглянуть на пример с spawn_link, давайте разберемся, что происходит при сбое процесса.
spawn fn -> raise "oops" end
Когда вышеуказанная программа запущена, она выдает следующую ошибку:
[error] Process #PID<0.58.00> raised an exception ** (RuntimeError) oops :erlang.apply/2
В журнале произошла ошибка, но процесс создания все еще выполняется. Это потому, что процессы изолированы. Если мы хотим, чтобы сбой в одном процессе распространялся на другой, нам нужно связать их. Это можно сделать с помощью функции spawn_link . Давайте рассмотрим пример, чтобы понять то же самое —
spawn_link fn -> raise "oops" end
Когда вышеуказанная программа запущена, она выдает следующую ошибку:
** (EXIT from #PID<0.41.0>) an exception was raised: ** (RuntimeError) oops :erlang.apply/2
Если вы выполняете это в оболочке iex, то оболочка обрабатывает эту ошибку и не завершается . Но если вы запустите сначала создание файла сценария, а затем с помощью elixir <имя-файла> .exs , родительский процесс также будет остановлен из-за этого сбоя.
Процессы и ссылки играют важную роль при построении отказоустойчивых систем. В приложениях Elixir мы часто связываем наши процессы с супервизорами, которые будут определять, когда процесс умирает, и запускать новый процесс на его месте. Это возможно только потому, что процессы изолированы и по умолчанию ничего не делят. А поскольку процессы изолированы, сбой в одном процессе не приведет к сбою или повреждению состояния другого. В то время как другие языки потребуют от нас перехватывать / обрабатывать исключения; в Elixir у нас все нормально, если процессы не работают, потому что мы ожидаем, что супервизоры должным образом перезапустят наши системы.
государственный
Если вы создаете приложение, которое требует состояния, например, чтобы сохранить конфигурацию приложения, или вам нужно проанализировать файл и сохранить его в памяти, где бы вы его сохранили? Функциональность процесса Elixir может пригодиться при выполнении таких задач.
Мы можем писать процессы, которые зацикливаются, поддерживают состояние, а также отправлять и получать сообщения. В качестве примера давайте напишем модуль, который запускает новые процессы, которые работают как хранилище значений ключей, в файле с именем kv.exs .
defmodule KV do def start_link do Task.start_link(fn -> loop(%{}) end) end defp loop(map) do receive do {:get, key, caller} -> send caller, Map.get(map, key) loop(map) {:put, key, value} -> loop(Map.put(map, key, value)) end end end
Обратите внимание, что функция start_link запускает новый процесс, который запускает функцию цикла , начиная с пустой карты. Затем функция цикла ожидает сообщения и выполняет соответствующее действие для каждого сообщения. В случае сообщения : get , оно отправляет сообщение обратно вызывающей стороне и снова вызывает loop, чтобы дождаться нового сообщения. В то время как сообщение : put фактически вызывает цикл с новой версией карты, с заданным ключом и значением, сохраненным.
Давайте теперь запустим следующее —
iex kv.exs
Теперь вы должны быть в вашей оболочке iex . Чтобы проверить наш модуль, попробуйте следующее —
{:ok, pid} = KV.start_link # pid now has the pid of our new process that is being # used to get and store key value pairs # Send a KV pair :hello, "Hello" to the process send pid, {:put, :hello, "Hello"} # Ask for the key :hello send pid, {:get, :hello, self()} # Print all the received messages on the current process. flush()
Когда вышеуказанная программа запущена, она дает следующий результат —
"Hello"
Эликсир — Сигилс
В этой главе мы собираемся изучить сигилы, механизмы, предоставляемые языком для работы с текстовыми представлениями. Символы начинаются с символа тильды (~), за которым следует буква (обозначающая символ), а затем разделитель; по желанию, модификаторы могут быть добавлены после окончательного разделителя.
Regex
Регулярные выражения в эликсире — это сигилы. Мы видели их использование в главе String. Давайте снова возьмем пример, чтобы увидеть, как мы можем использовать регулярные выражения в эликсире.
# A regular expression that matches strings which contain "foo" or # "bar": regex = ~r/foo|bar/ IO.puts("foo" =~ regex) IO.puts("baz" =~ regex)
Когда вышеуказанная программа запущена, она дает следующий результат —
true false
Сигилы поддерживают 8 различных разделителей —
~r/hello/ ~r|hello| ~r"hello" ~r'hello' ~r(hello) ~r[hello] ~r{hello} ~r<hello>
Причина поддержки разных разделителей состоит в том, что разные разделители могут быть более подходящими для разных символов. Например, использование скобок для регулярных выражений может быть запутанным выбором, поскольку они могут смешиваться с круглыми скобками внутри регулярного выражения. Тем не менее, скобки могут быть полезны для других символов, как мы увидим в следующем разделе.
Elixir поддерживает Perl-совместимые регулярные выражения, а также поддерживает модификаторы. Вы можете прочитать больше об использовании регулярных выражений здесь .
Строки, списки символов и списки слов
Помимо регулярных выражений, в Elixir есть еще 3 встроенных сигилы. Давайте посмотрим на сигилы.
Струны
Символ s используется для генерации строк, как двойные кавычки. Символ ~ s полезен, например, когда строка содержит как двойные, так и одинарные кавычки —
new_string = ~s(this is a string with "double" quotes, not 'single' ones) IO.puts(new_string)
Этот символ генерирует строки. Когда вышеуказанная программа запущена, она дает следующий результат —
"this is a string with \"double\" quotes, not 'single' ones"
Списки символов
Символ ~ c используется для создания списков символов —
new_char_list = ~c(this is a char list containing 'single quotes') IO.puts(new_char_list)
Когда вышеуказанная программа запущена, она дает следующий результат —
this is a char list containing 'single quotes'
Списки слов
Символ ~ w используется для генерации списков слов (слова являются обычными строками). В символе ~ w слова разделяются пробелами.
new_word_list = ~w(foo bar bat) IO.puts(new_word_list)
Когда вышеуказанная программа запущена, она дает следующий результат —
foobarbat
Символ ~ w также принимает модификаторы c, s и a (для списков символов, строк и атомов соответственно), которые указывают тип данных элементов результирующего списка —
new_atom_list = ~w(foo bar bat)a IO.puts(new_atom_list)
Когда вышеуказанная программа запущена, она дает следующий результат —
[:foo, :bar, :bat]
Интерполяция и побег в сигилах
Помимо строчных символов, Elixir поддерживает прописные символы для работы с экранирующими символами и интерполяцией. Хотя и ~ s, и ~ S будут возвращать строки, первый допускает escape-коды и интерполяцию, а второй — нет. Давайте рассмотрим пример, чтобы понять это —
~s(String with escape codes \x26 #{"inter" <> "polation"}) # "String with escape codes & interpolation" ~S(String without escape codes \x26 without #{interpolation}) # "String without escape codes \\x26 without \#{interpolation}"
Пользовательские сигилы
Мы можем легко создавать свои собственные сигилы. В этом примере мы создадим сигил для преобразования строки в верхний регистр.
defmodule CustomSigil do def sigil_u(string, []), do: String.upcase(string) end import CustomSigil IO.puts(~u/tutorials point/)
Когда мы запускаем приведенный выше код, он дает следующий результат —
TUTORIALS POINT
Сначала мы определяем модуль с именем CustomSigil, и внутри этого модуля мы создали функцию с именем sigil_u. Поскольку в существующем пространстве сигилов нет существующего символа, мы будем его использовать. _U указывает, что мы хотим использовать вас как символ после тильды. Определение функции должно принимать два аргумента, входные данные и список.
Эликсир — Понимание
Понимание списка — синтаксический сахар для циклического перебора перечислимых в эликсире. В этой главе мы будем использовать понимания для итерации и генерации.
основы
Когда мы посмотрели на модуль Enum в главе enumerables, мы натолкнулись на функцию map.
Enum.map(1..3, &(&1 * 2))
В этом примере мы передадим функцию в качестве второго аргумента. Каждый элемент в диапазоне будет передан в функцию, а затем будет возвращен новый список, содержащий новые значения.
Картирование, фильтрация и преобразование являются очень распространенными действиями в Elixir, и поэтому существует несколько иной способ достижения того же результата, что и в предыдущем примере —
for n <- 1..3, do: n * 2
Когда мы запускаем приведенный выше код, он дает следующий результат —
[2, 4, 6]
Второй пример — это понимание, и, как вы, вероятно, можете видеть, это просто синтаксический сахар для того, чего вы также можете достичь, если используете функцию Enum.map . Тем не менее, нет никаких реальных преимуществ использования понимания по сравнению с функцией из модуля Enum с точки зрения производительности.
Понимания не ограничиваются списками, но могут использоваться со всеми перечислимыми.
Фильтр
Вы можете думать о фильтрах как о некоей страже понимания. Когда отфильтрованное значение возвращает false или nil, оно исключается из окончательного списка. Давайте переберем диапазон и будем беспокоиться только о четных числах. Мы будем использовать функцию is_even из модуля Integer, чтобы проверить, является ли значение четным или нет.
import Integer IO.puts(for x <- 1..10, is_even(x), do: x)
Когда приведенный выше код выполняется, он дает следующий результат —
[2, 4, 6, 8, 10]
Мы также можем использовать несколько фильтров в одном понимании. Добавьте еще один фильтр после фильтра is_even, разделенного запятой.
: в вариант
В приведенных выше примерах все понимания возвращали списки в качестве результата. Тем не менее, результат понимания может быть вставлен в различные структуры данных путем передачи опции : into в понимание.
Например, генератор цепочки битов может использоваться с опцией: into, чтобы легко удалить все пробелы в строке —
IO.puts(for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>>)
Когда приведенный выше код выполняется, он дает следующий результат —
helloworld
Приведенный выше код удаляет все пробелы из строки с помощью фильтра c! =? \ S, а затем с помощью параметра: into помещает все возвращаемые символы в строку.
Эликсир — Typespecs
Elixir — это динамически типизированный язык, поэтому все типы в Elixir выводятся во время выполнения. Тем не менее, Elixir поставляется с типами спецификаций, которые используются для объявления пользовательских типов данных и сигнатур типизированных функций (спецификаций) .
Функциональные характеристики (спецификации)
По умолчанию Elixir предоставляет несколько основных типов, таких как целое число или pid, а также сложные типы: например, функция округления , которая округляет число с плавающей точкой до ближайшего целого числа, принимает число в качестве аргумента (целое число или число с плавающей запятой) и возвращает целое число В соответствующей документации круглая печатная подпись записывается как —
round(number) :: integer
Вышеприведенное описание подразумевает, что функция слева принимает в качестве аргумента то, что указано в скобках, и возвращает то, что справа от ::, то есть Integer. Спецификации функций записываются с помощью директивы @spec , расположенной прямо перед определением функции. Функция округления может быть записана как —
@spec round(number) :: integer def round(number), do: # Function implementation ...
Типы также поддерживают сложные типы, например, если вы хотите вернуть список целых чисел, вы можете использовать [Integer]
Пользовательские типы
В то время как Elixir предоставляет много полезных встроенных типов, удобно определять пользовательские типы при необходимости. Это можно сделать при определении модулей с помощью директивы @type. Давайте рассмотрим пример, чтобы понять то же самое —
defmodule FunnyCalculator do @type number_with_joke :: {number, String.t} @spec add(number, number) :: number_with_joke def add(x, y), do: {x + y, "You need a calculator to do that?"} @spec multiply(number, number) :: number_with_joke def multiply(x, y), do: {x * y, "It is like addition on steroids."} end {result, comment} = FunnyCalculator.add(10, 20) IO.puts(result) IO.puts(comment)
Когда вышеуказанная программа запущена, она дает следующий результат —
30 You need a calculator to do that?
ПРИМЕЧАНИЕ. — Пользовательские типы, определенные с помощью @type, экспортируются и доступны вне модуля, в котором они определены. Если вы хотите сохранить пользовательский тип закрытым, вы можете использовать директиву @typep вместо @type .
Эликсир — Поведение
Поведения в Elixir (и Erlang) — это способ отделить и абстрагировать общую часть компонента (которая становится модулем поведения) от конкретной части (которая становится модулем обратного вызова). Поведение обеспечивает способ —
- Определите набор функций, которые должны быть реализованы модулем.
- Убедитесь, что модуль реализует все функции в этом наборе.
Если вам нужно, вы можете подумать о поведении, подобном интерфейсам в объектно-ориентированных языках, таких как Java: набор сигнатур функций, которые должен реализовывать модуль.
Определение поведения
Давайте рассмотрим пример для создания нашего собственного поведения, а затем используем это общее поведение для создания модуля. Мы определим поведение, которое приветствует людей на разных языках.
defmodule GreetBehaviour do @callback say_hello(name :: string) :: nil @callback say_bye(name :: string) :: nil end
Директива @callback используется для перечисления функций, которые нужно будет определить принимающим модулям. Также указывается нет. аргументов, их тип и их возвращаемые значения.
Принятие Поведения
Мы успешно определили поведение. Теперь мы примем и внедрим его в несколько модулей. Давайте создадим два модуля, реализующих это поведение на английском и испанском языках.
defmodule GreetBehaviour do @callback say_hello(name :: string) :: nil @callback say_bye(name :: string) :: nil end defmodule EnglishGreet do @behaviour GreetBehaviour def say_hello(name), do: IO.puts("Hello " <> name) def say_bye(name), do: IO.puts("Goodbye, " <> name) end defmodule SpanishGreet do @behaviour GreetBehaviour def say_hello(name), do: IO.puts("Hola " <> name) def say_bye(name), do: IO.puts("Adios " <> name) end EnglishGreet.say_hello("Ayush") EnglishGreet.say_bye("Ayush") SpanishGreet.say_hello("Ayush") SpanishGreet.say_bye("Ayush")
Когда вышеуказанная программа запущена, она дает следующий результат —
Hello Ayush Goodbye, Ayush Hola Ayush Adios Ayush
Как вы уже видели, мы применяем поведение, используя директиву @behaviour в модуле. Мы должны определить все функции, реализованные в поведении для всех дочерних модулей. Это можно грубо считать эквивалентным интерфейсам на языках ООП.
Эликсир — Обработка ошибок
У Elixir есть три механизма ошибок: ошибки, броски и выходы. Давайте рассмотрим каждый механизм подробно.
ошибка
Ошибки (или исключения) используются, когда в коде происходят исключительные вещи. Пример ошибки можно получить, если попытаться добавить число в строку —
IO.puts(1 + "Hello")
Когда вышеуказанная программа запущена, она выдает следующую ошибку:
** (ArithmeticError) bad argument in arithmetic expression :erlang.+(1, "Hello")
Это была встроенная ошибка образца.
Повышение ошибок
Мы можем поднять ошибки, используя функции повышения. Давайте рассмотрим пример, чтобы понять то же самое —
#Runtime Error with just a message raise "oops" # ** (RuntimeError) oops
Другие ошибки могут быть вызваны с помощью параметра allow / 2, передающего имя ошибки и список ключевых аргументов.
#Other error type with a message raise ArgumentError, message: "invalid argument foo"
Вы также можете определить свои собственные ошибки и поднять их. Рассмотрим следующий пример —
defmodule MyError do defexception message: "default message" end raise MyError # Raises error with default message raise MyError, message: "custom message" # Raises error with custom message
Спасая ошибки
Мы не хотим, чтобы наши программы внезапно закрывались, скорее, ошибки должны тщательно обрабатываться. Для этого мы используем обработку ошибок. Мы спасаем ошибки, используя конструкцию try / rescue . Давайте рассмотрим следующий пример, чтобы понять то же самое —
err = try do raise "oops" rescue e in RuntimeError -> e end IO.puts(err.message)
Когда вышеуказанная программа запущена, она дает следующий результат —
oops
Мы обработали ошибки в операторе спасения, используя сопоставление с образцом. Если мы не используем ошибку и просто хотим использовать ее для идентификации, мы также можем использовать форму:
err = try do 1 + "Hello" rescue RuntimeError -> "You've got a runtime error!" ArithmeticError -> "You've got a Argument error!" end IO.puts(err)
При запуске вышеуказанной программы, она дает следующий результат —
You've got a Argument error!
ПРИМЕЧАНИЕ. — Большинство функций в стандартной библиотеке Elixir реализованы дважды: один раз возвращает кортежи, а другой — ошибки. Например, File.read и File.read! функции. Первый вернул кортеж, если файл был успешно прочитан, и если произошла ошибка, этот кортеж использовался, чтобы указать причину ошибки. Второй вызвал ошибку, если возникла ошибка.
Если мы используем подход первой функции, то нам нужно использовать case для сопоставления с шаблоном ошибки и действовать в соответствии с этим. Во втором случае мы используем подход try rescue для подверженного ошибкам кода и соответственно обрабатываем ошибки.
Броски
В эликсире значение может быть выброшено, а затем поймано. Throw и Catch зарезервированы для ситуаций, когда невозможно извлечь значение, если не использовать throw и catch.
Практические примеры довольно необычны, за исключением случаев взаимодействия с библиотеками. Например, давайте теперь предположим, что модуль Enum не предоставил никакого API для поиска значения и что нам нужно было найти первое кратное 13 в списке чисел —
val = try do Enum.each 20..100, fn(x) -> if rem(x, 13) == 0, do: throw(x) end "Got nothing" catch x -> "Got #{x}" end IO.puts(val)
Когда вышеуказанная программа запущена, она дает следующий результат —
Got 26
Выход
Когда процесс умирает по «естественным причинам» (например, необработанные исключения), он посылает сигнал выхода. Процесс также может умереть, явно отправив сигнал на выход. Давайте рассмотрим следующий пример —
spawn_link fn -> exit(1) end
В приведенном выше примере связанный процесс умер, отправив сигнал выхода со значением 1. Обратите внимание, что выход также можно «перехватить» с помощью try / catch. Например —
val = try do exit "I am exiting" catch :exit, _ -> "not really" end IO.puts(val)
Когда вышеуказанная программа запущена, она дает следующий результат —
not really
После
Иногда необходимо убедиться, что ресурс очищен после некоторого действия, которое потенциально может вызвать ошибку. Конструкция try / after позволяет вам это сделать. Например, мы можем открыть файл и использовать предложение after, чтобы закрыть его, даже если что-то пойдет не так.
{:ok, file} = File.open "sample", [:utf8, :write] try do IO.write file, "olá" raise "oops, something went wrong" after File.close(file) end
Когда мы запустим эту программу, она выдаст нам ошибку. Но оператор after гарантирует, что дескриптор файла будет закрыт при любом таком событии.
Эликсир — Макросы
Макросы являются одной из самых продвинутых и мощных функций Elixir. Как и со всеми расширенными функциями любого языка, макросы следует использовать с осторожностью. Они позволяют выполнять мощные преобразования кода во время компиляции. Теперь мы поймем, что такое макросы и как их использовать вкратце.
котировка
Прежде чем мы начнем говорить о макросах, давайте сначала посмотрим на внутренности Elixir. Программа Elixir может быть представлена своими собственными структурами данных. Строительным блоком программы Elixir является кортеж из трех элементов. Например, сумма вызова функции (1, 2, 3) представлена внутри как —
{:sum, [], [1, 2, 3]}
Первый элемент — это имя функции, второй — список ключевых слов, содержащий метаданные, а третий — список аргументов. Вы можете получить это как вывод в оболочке iex, если напишите следующее:
quote do: sum(1, 2, 3)
Операторы также представлены в виде таких кортежей. Переменные также представлены с использованием таких триплетов, за исключением того, что последний элемент является атомом, а не списком. При цитировании более сложных выражений мы можем видеть, что код представлен в таких кортежах, которые часто вкладываются друг в друга в структуру, напоминающую дерево. Многие языки называют такие представления абстрактным синтаксическим деревом (AST) . Эликсир называет эти цитируемые выражения.
закрывать кавычки
Теперь, когда мы можем получить внутреннюю структуру нашего кода, как мы можем его изменить? Чтобы ввести новый код или значения, мы используем кавычки . Когда мы цитируем выражение, оно будет оценено и введено в AST. Давайте рассмотрим пример (в оболочке iex), чтобы понять концепцию —
num = 25 quote do: sum(15, num) quote do: sum(15, unquote(num))
Когда вышеуказанная программа запущена, она дает следующий результат —
{:sum, [], [15, {:num, [], Elixir}]} {:sum, [], [15, 25]}
В примере для выражения кавычки оно не заменяло автоматически num на 25. Нам нужно снять кавычки этой переменной, если мы хотим изменить AST.
макрос
Итак, теперь, когда мы знакомы с кавычками и кавычками, мы можем исследовать метапрограммирование в Elixir с помощью макросов.
В простейших терминах макросы — это специальные функции, предназначенные для возврата выражения в кавычках, которое будет вставлено в код нашего приложения. Представьте, что макрос заменяется выражением в кавычках, а не вызывается как функция. С помощью макросов у нас есть все необходимое для расширения Elixir и динамического добавления кода в наши приложения.
Давайте реализовывать разве что в качестве макроса. Мы начнем с определения макроса с помощью макроса defmacro . Помните, что наш макрос должен возвращать выражение в кавычках.
defmodule OurMacro do defmacro unless(expr, do: block) do quote do if !unquote(expr), do: unquote(block) end end end require OurMacro OurMacro.unless true, do: IO.puts "True Expression" OurMacro.unless false, do: IO.puts "False expression"
Когда вышеуказанная программа запущена, она дает следующий результат —
False expression
Здесь происходит то, что наш код заменяется кодом в кавычках, возвращаемым макросом Мы произвели кавычку в выражении, чтобы оценить его в текущем контексте, а также в кавычках блок do, чтобы выполнить его в своем контексте. Этот пример показывает нам метапрограммирование с использованием макросов в эликсире.
Макросы могут использоваться в гораздо более сложных задачах, но их следует использовать с осторожностью. Это связано с тем, что метапрограммирование в целом считается плохой практикой и должно использоваться только при необходимости.
Эликсир — Библиотеки
Elixir обеспечивает отличную совместимость с библиотеками Erlang. Давайте кратко обсудим несколько библиотек.
Двоичный модуль
Встроенный модуль Elixir String обрабатывает двоичные файлы в кодировке UTF-8. Двоичный модуль полезен, когда вы имеете дело с двоичными данными, которые не обязательно кодируются в UTF-8. Давайте рассмотрим пример для дальнейшего понимания двоичного модуля —
# UTF-8 IO.puts(String.to_char_list("Ø")) # binary IO.puts(:binary.bin_to_list "Ø")
Когда вышеуказанная программа запущена, она дает следующий результат —
[216] [195, 152]
Приведенный выше пример показывает разницу; модуль String возвращает кодовые точки UTF-8, в то время как: двоичный файл обрабатывает необработанные байты данных.
Крипто модуль
Криптомодуль содержит функции хеширования, цифровые подписи, шифрование и многое другое. Этот модуль не является частью стандартной библиотеки Erlang, но включен в дистрибутив Erlang. Это означает, что вы должны указывать: crypto в списке приложений вашего проекта всякий раз, когда вы его используете. Давайте посмотрим на пример с использованием модуля crypto —
IO.puts(Base.encode16(:crypto.hash(:sha256, "Elixir")))
Когда вышеуказанная программа запущена, она дает следующий результат —
3315715A7A3AD57428298676C5AE465DADA38D951BDFAC9348A8A31E9C7401CB
Модуль Digraph
Модуль digraph содержит функции для работы с ориентированными графами, построенными из вершин и ребер. После построения графа алгоритмы в нем помогут найти, например, кратчайший путь между двумя вершинами или петлями в графе. Обратите внимание, что функции в: digraph косвенно изменяют структуру графа как побочный эффект, возвращая добавленные вершины или ребра.
digraph = :digraph.new() coords = [{0.0, 0.0}, {1.0, 0.0}, {1.0, 1.0}] [v0, v1, v2] = (for c <- coords, do: :digraph.add_vertex(digraph, c)) :digraph.add_edge(digraph, v0, v1) :digraph.add_edge(digraph, v1, v2) for point <- :digraph.get_short_path(digraph, v0, v2) do {x, y} = point IO.puts("#{x}, #{y}") end
Когда вышеуказанная программа запущена, она дает следующий результат —
0.0, 0.0 1.0, 0.0 1.0, 1.0
Математический модуль
Математический модуль содержит общие математические операции, охватывающие тригонометрию, экспоненциальные и логарифмические функции. Давайте рассмотрим следующий пример, чтобы понять, как работает модуль Math:
# Value of pi IO.puts(:math.pi()) # Logarithm IO.puts(:math.log(7.694785265142018e23)) # Exponentiation IO.puts(:math.exp(55.0)) #...
Когда вышеуказанная программа запущена, она дает следующий результат —
3.141592653589793 55.0 7.694785265142018e23
Модуль очереди
Очередь — это структура данных, которая эффективно реализует (двусторонние) очереди FIFO (первым пришел-первым вышел). В следующем примере показано, как работает модуль очереди.
q = :queue.new q = :queue.in("A", q) q = :queue.in("B", q) {{:value, val}, q} = :queue.out(q) IO.puts(val) {{:value, val}, q} = :queue.out(q) IO.puts(val)
Когда вышеуказанная программа запущена, она дает следующий результат —