# Однострочные комментарии начинаются с символа решётки. # Для многострочных комментариев отдельного синтаксиса нет, # поэтому просто используйте несколько однострочных комментариев. # Запустить интерактивную Elixir-консоль (аналог `irb` в Ruby) можно # при помощи команды `iex`. # Чтобы скомпилировать модуль, воспользуйтесь командой `elixirc`. # Обе команды будут работать из терминала, если вы правильно установили Elixir. ## --------------------------- ## -- Базовые типы ## --------------------------- # Числа 3 # целое число 0x1F # целое число 3.0 # число с плавающей запятой # Атомы, которые являются нечисловыми константами. Они начинаются с символа `:`. :hello # атом # Кортежи, которые хранятся в памяти последовательно. {1,2,3} # кортеж # Получить доступ к элементу кортежа мы можем с помощью функции `elem`: elem({1, 2, 3}, 0) #=> 1 # Списки, которые реализованы как связные списки. [1,2,3] # список # У каждого непустого списка есть голова (первый элемент списка) # и хвост (все остальные элементы списка): [head | tail] = [1,2,3] head #=> 1 tail #=> [2,3] # В Elixir, как и в Erlang, знак `=` служит для сопоставления с образцом, # а не для операции присваивания. # # Это означает, что выражение слева от знака `=` (образец) сопоставляется с # выражением справа. # # Сопоставление с образцом позволило нам получить голову и хвост списка # в примере выше. # Если выражения слева и справа от знака `=` не удаётся сопоставить, будет # брошена ошибка. Например, если кортежи разных размеров. {a, b, c} = {1, 2} #=> ** (MatchError) # Бинарные данные <<1,2,3>> # Вы столкнётесь с двумя видами строк: "hello" # Elixir-строка (заключена в двойные кавычки) 'hello' # Erlang-строка (заключена в одинарные кавычки) # Все строки представлены в кодировке UTF-8: "привет" #=> "привет" # Многострочный текст """ Я текст на несколько строк. """ #=> "Я текст на несколько\nстрок.\n" # Чем Elixir-строки отличаются от Erlang-строк? Elixir-строки являются бинарными # данными. <> #=> "abc" # Erlang-строка — это на самом деле список. [?a, ?b, ?c] #=> 'abc' # Оператор `?` возвращает целое число, соответствующее данному символу. ?a #=> 97 # Для объединения бинарных данных (и Elixir-строк) используйте `<>` <<1,2,3>> <> <<4,5>> #=> <<1,2,3,4,5>> "hello " <> "world" #=> "hello world" # Для объединения списков (и Erlang-строк) используйте `++` [1,2,3] ++ [4,5] #=> [1,2,3,4,5] 'hello ' ++ 'world' #=> 'hello world' # Диапазоны записываются как `начало..конец` (оба включительно) 1..10 #=> 1..10 # Сопоставление с образцом применимо и для диапазонов: lower..upper = 1..10 [lower, upper] #=> [1, 10] # Карты (известны вам по другим языкам как ассоциативные массивы, словари, хэши) genders = %{"david" => "male", "gillian" => "female"} genders["david"] #=> "male" # Для карт, где ключами выступают атомы, доступен специальный синтаксис genders = %{david: "male", gillian: "female"} genders.gillian #=> "female" ## --------------------------- ## -- Операторы ## --------------------------- # Математические операции 1 + 1 #=> 2 10 - 5 #=> 5 5 * 2 #=> 10 10 / 2 #=> 5.0 # В Elixir оператор `/` всегда возвращает число с плавающей запятой. # Для целочисленного деления применяйте `div` div(10, 2) #=> 5 # Для получения остатка от деления к вашим услугам `rem` rem(10, 3) #=> 1 # Булевые операторы: `or`, `and`, `not`. # В качестве первого аргумента эти операторы ожидают булевое значение. true and true #=> true false or true #=> true 1 and true #=> ** (BadBooleanError) # Elixir также предоставляет `||`, `&&` и `!`, которые принимают аргументы # любого типа. Всё, кроме `false` и `nil`, считается `true`. 1 || true #=> 1 false && 1 #=> false nil && 20 #=> nil !true #=> false # Операторы сравнения: `==`, `!=`, `===`, `!==`, `<=`, `>=`, `<`, `>` 1 == 1 #=> true 1 != 1 #=> false 1 < 2 #=> true # Операторы `===` и `!==` более строгие. Разница заметна, когда мы сравниваем # числа целые и с плавающей запятой: 1 == 1.0 #=> true 1 === 1.0 #=> false # Elixir позволяет сравнивать значения разных типов: 1 < :hello #=> true # При сравнении разных типов руководствуйтесь следующим правилом: # число < атом < ссылка < функция < порт < процесс < кортеж < список < строка ## --------------------------- ## -- Порядок выполнения ## --------------------------- # Условный оператор `if` if false do "Вы этого никогда не увидите" else "Вы увидите это" end # Противоположный ему условный оператор `unless` unless true do "Вы этого никогда не увидите" else "Вы увидите это" end # Помните сопоставление с образцом? # Многие конструкции в Elixir построены вокруг него. # `case` позволяет сравнить выражение с несколькими образцами: case {:one, :two} do {:four, :five} -> "Этот образец не совпадёт" {:one, x} -> "Этот образец совпадёт и присвоит переменной `x` значение `:two`" _ -> "Этот образец совпадёт с чем угодно" end # Символ `_` называется анонимной переменной. Используйте `_` для значений, # которые в текущем выражении вас не интересуют. Например, вам интересна лишь # голова списка, а хвост вы желаете проигнорировать: [head | _] = [1,2,3] head #=> 1 # Для лучшей читаемости вы можете написать: [head | _tail] = [:a, :b, :c] head #=> :a # `cond` позволяет проверить сразу несколько условий за раз. # Используйте `cond` вместо множественных операторов `if`. cond do 1 + 1 == 3 -> "Вы меня никогда не увидите" 2 * 5 == 12 -> "И меня" 1 + 2 == 3 -> "Вы увидите меня" end # Обычно последним условием идёт `true`, которое выполнится, если все предыдущие # условия оказались ложны. cond do 1 + 1 == 3 -> "Вы меня никогда не увидите" 2 * 5 == 12 -> "И меня" true -> "Вы увидите меня (по сути, это `else`)" end # Обработка ошибок происходит в блоках `try/catch`. # Elixir также поддерживает блок `after`, который выполнится в любом случае. try do throw(:hello) catch message -> "Поймана ошибка с сообщением #{message}." after IO.puts("Я выполнюсь всегда") end #=> Я выполнюсь всегда # "Поймана ошибка с сообщением hello." ## --------------------------- ## -- Модули и функции ## --------------------------- # Анонимные функции (обратите внимание на точку при вызове функции) square = fn(x) -> x * x end square.(5) #=> 25 # Анонимные функции принимают клозы и гарды. # # Клозы (от англ. clause) — варианты исполнения функции. # # Гарды (от англ. guard) — охранные выражения, уточняющие сопоставление с # образцом в функциях. Гарды следуют после ключевого слова `when`. f = fn x, y when x > 0 -> x + y x, y -> x * y end f.(1, 3) #=> 4 f.(-1, 3) #=> -3 # В Elixir много встроенных функций. # Они доступны в текущей области видимости. is_number(10) #=> true is_list("hello") #=> false elem({1,2,3}, 0) #=> 1 # Вы можете объединить несколько функций в модуль. Внутри модуля используйте `def`, # чтобы определить свои функции. defmodule Math do def sum(a, b) do a + b end def square(x) do x * x end end Math.sum(1, 2) #=> 3 Math.square(3) #=> 9 # Чтобы скомпилировать модуль Math, сохраните его в файле `math.ex` # и наберите в терминале: `elixirc math.ex` defmodule PrivateMath do # Публичные функции начинаются с `def` и доступны из других модулей. def sum(a, b) do do_sum(a, b) end # Приватные функции начинаются с `defp` и доступны только внутри своего модуля. defp do_sum(a, b) do a + b end end PrivateMath.sum(1, 2) #=> 3 PrivateMath.do_sum(1, 2) #=> ** (UndefinedFunctionError) # Функции внутри модуля тоже принимают клозы и гарды defmodule Geometry do def area({:rectangle, w, h}) do w * h end def area({:circle, r}) when is_number(r) do 3.14 * r * r end end Geometry.area({:rectangle, 2, 3}) #=> 6 Geometry.area({:circle, 3}) #=> 28.25999999999999801048 Geometry.area({:circle, "not_a_number"}) #=> ** (FunctionClauseError) # Из-за неизменяемых переменных в Elixir важную роль играет рекурсия defmodule Recursion do def sum_list([head | tail], acc) do sum_list(tail, acc + head) end def sum_list([], acc) do acc end end Recursion.sum_list([1,2,3], 0) #=> 6 # Модули в Elixir поддерживают атрибуты. # Атрибуты бывают как встроенные, так и ваши собственные. defmodule MyMod do @moduledoc """ Это встроенный атрибут """ @my_data 100 # А это ваш атрибут IO.inspect(@my_data) #=> 100 end # Одна из фишек языка — оператор `|>` # Он передаёт выражение слева в качестве первого аргумента функции справа: Range.new(1,10) |> Enum.map(fn x -> x * x end) |> Enum.filter(fn x -> rem(x, 2) == 0 end) #=> [4, 16, 36, 64, 100] ## --------------------------- ## -- Структуры и исключения ## --------------------------- # Структуры — это расширения поверх карт, привносящие в Elixir значения по # умолчанию, проверки на этапе компиляции и полиморфизм. defmodule Person do defstruct name: nil, age: 0, height: 0 end joe_info = %Person{ name: "Joe", age: 30, height: 180 } #=> %Person{age: 30, height: 180, name: "Joe"} # Доступ к полю структуры joe_info.name #=> "Joe" # Обновление поля структуры older_joe_info = %{ joe_info | age: 31 } #=> %Person{age: 31, height: 180, name: "Joe"} # Блок `try` с ключевым словом `rescue` используется для обработки исключений try do raise "какая-то ошибка" rescue RuntimeError -> "перехвачена ошибка рантайма" _error -> "перехват любой другой ошибки" end #=> "перехвачена ошибка рантайма" # У каждого исключения есть сообщение try do raise "какая-то ошибка" rescue x in [RuntimeError] -> x.message end #=> "какая-то ошибка" ## --------------------------- ## -- Параллелизм ## --------------------------- # Параллелизм в Elixir построен на модели акторов. Для написания # параллельной программы нам понадобятся три вещи: # 1. Создание процессов # 2. Отправка сообщений # 3. Приём сообщений # Новый процесс создаётся функцией `spawn`, которая принимает функцию # в качестве аргумента. f = fn -> 2 * 2 end #=> #Function spawn(f) #=> #PID<0.40.0> # `spawn` возвращает идентификатор процесса (англ. process identifier, PID). # Вы можете использовать PID для отправки сообщений этому процессу. Сообщения # отправляются через оператор `send`. А для приёма сообщений используется # механизм `receive`: # Блок `receive do` ждёт сообщений и обработает их, как только получит. Блок # `receive do` обработает лишь одно полученное сообщение. Чтобы обработать # несколько сообщений, функция, содержащая блок `receive do`, должна рекурсивно # вызывать себя. defmodule Geometry do def area_loop do receive do {:rectangle, w, h} -> IO.puts("Площадь = #{w * h}") area_loop() {:circle, r} -> IO.puts("Площадь = #{3.14 * r * r}") area_loop() end end end # Скомпилируйте модуль и создайте процесс pid = spawn(fn -> Geometry.area_loop() end) #=> #PID<0.40.0> # Альтернативно pid = spawn(Geometry, :area_loop, []) # Отправьте сообщение процессу send pid, {:rectangle, 2, 3} #=> Площадь = 6 # {:rectangle,2,3} send pid, {:circle, 2} #=> Площадь = 12.56 # {:circle,2} # Кстати, интерактивная консоль — это тоже процесс. # Чтобы узнать текущий PID, воспользуйтесь встроенной функцией `self` self() #=> #PID<0.27.0> ## --------------------------- ## -- Агенты ## --------------------------- # Агент — это процесс, который следит за некоторым изменяющимся значением. # Создайте агента через `Agent.start_link`, передав ему функцию. # Начальным состоянием агента будет значение, которое эта функция возвращает. {ok, my_agent} = Agent.start_link(fn -> ["красный", "зелёный"] end) # `Agent.get` принимает имя агента и анонимную функцию `fn`, которой будет # передано текущее состояние агента. В результате вы получите то, что вернёт # анонимная функция. Agent.get(my_agent, fn colors -> colors end) #=> ["красный", "зелёный"] # Похожим образом вы можете обновить состояние агента Agent.update(my_agent, fn colors -> ["синий" | colors] end)