elixir.md

Личный сайт Go-разработчика из Казани

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

1# Однострочные комментарии начинаются с символа решётки. 2 3# Для многострочных комментариев отдельного синтаксиса нет, 4# поэтому просто используйте несколько однострочных комментариев. 5 6# Запустить интерактивную Elixir-консоль (аналог `irb` в Ruby) можно 7# при помощи команды `iex`. 8# Чтобы скомпилировать модуль, воспользуйтесь командой `elixirc`. 9 10# Обе команды будут работать из терминала, если вы правильно установили Elixir. 11 12## --------------------------- 13## -- Базовые типы 14## --------------------------- 15 16# Числа 173 # целое число 180x1F # целое число 193.0 # число с плавающей запятой 20 21# Атомы, которые являются нечисловыми константами. Они начинаются с символа `:`. 22:hello # атом 23 24# Кортежи, которые хранятся в памяти последовательно. 25{1,2,3} # кортеж 26 27# Получить доступ к элементу кортежа мы можем с помощью функции `elem`: 28elem({1, 2, 3}, 0) #=> 1 29 30# Списки, которые реализованы как связные списки. 31[1,2,3] # список 32 33# У каждого непустого списка есть голова (первый элемент списка) 34# и хвост (все остальные элементы списка): 35[head | tail] = [1,2,3] 36head #=> 1 37tail #=> [2,3] 38 39# В Elixir, как и в Erlang, знак `=` служит для сопоставления с образцом, 40# а не для операции присваивания. 41# 42# Это означает, что выражение слева от знака `=` (образец) сопоставляется с 43# выражением справа. 44# 45# Сопоставление с образцом позволило нам получить голову и хвост списка 46# в примере выше. 47 48# Если выражения слева и справа от знака `=` не удаётся сопоставить, будет 49# брошена ошибка. Например, если кортежи разных размеров. 50{a, b, c} = {1, 2} #=> ** (MatchError) 51 52# Бинарные данные 53<<1,2,3>> 54 55# Вы столкнётесь с двумя видами строк: 56"hello" # Elixir-строка (заключена в двойные кавычки) 57'hello' # Erlang-строка (заключена в одинарные кавычки) 58 59# Все строки представлены в кодировке UTF-8: 60"привет" #=> "привет" 61 62# Многострочный текст 63""" 64Я текст на несколько 65строк. 66""" 67#=> "Я текст на несколько\nстрок.\n" 68 69# Чем Elixir-строки отличаются от Erlang-строк? Elixir-строки являются бинарными 70# данными. 71<<?a, ?b, ?c>> #=> "abc" 72# Erlang-строка — это на самом деле список. 73[?a, ?b, ?c] #=> 'abc' 74 75# Оператор `?` возвращает целое число, соответствующее данному символу. 76?a #=> 97 77 78# Для объединения бинарных данных (и Elixir-строк) используйте `<>` 79<<1,2,3>> <> <<4,5>> #=> <<1,2,3,4,5>> 80"hello " <> "world" #=> "hello world" 81 82# Для объединения списков (и Erlang-строк) используйте `++` 83[1,2,3] ++ [4,5] #=> [1,2,3,4,5] 84'hello ' ++ 'world' #=> 'hello world' 85 86# Диапазоны записываются как `начало..конец` (оба включительно) 871..10 #=> 1..10 88 89# Сопоставление с образцом применимо и для диапазонов: 90lower..upper = 1..10 91[lower, upper] #=> [1, 10] 92 93# Карты (известны вам по другим языкам как ассоциативные массивы, словари, хэши) 94genders = %{"david" => "male", "gillian" => "female"} 95genders["david"] #=> "male" 96 97# Для карт, где ключами выступают атомы, доступен специальный синтаксис 98genders = %{david: "male", gillian: "female"} 99genders.gillian #=> "female" 100 101## --------------------------- 102## -- Операторы 103## --------------------------- 104 105# Математические операции 1061 + 1 #=> 2 10710 - 5 #=> 5 1085 * 2 #=> 10 10910 / 2 #=> 5.0 110 111# В Elixir оператор `/` всегда возвращает число с плавающей запятой. 112 113# Для целочисленного деления применяйте `div` 114div(10, 2) #=> 5 115 116# Для получения остатка от деления к вашим услугам `rem` 117rem(10, 3) #=> 1 118 119# Булевые операторы: `or`, `and`, `not`. 120# В качестве первого аргумента эти операторы ожидают булевое значение. 121true and true #=> true 122false or true #=> true 1231 and true #=> ** (BadBooleanError) 124 125# Elixir также предоставляет `||`, `&&` и `!`, которые принимают аргументы 126# любого типа. Всё, кроме `false` и `nil`, считается `true`. 1271 || true #=> 1 128false && 1 #=> false 129nil && 20 #=> nil 130!true #=> false 131 132# Операторы сравнения: `==`, `!=`, `===`, `!==`, `<=`, `>=`, `<`, `>` 1331 == 1 #=> true 1341 != 1 #=> false 1351 < 2 #=> true 136 137# Операторы `===` и `!==` более строгие. Разница заметна, когда мы сравниваем 138# числа целые и с плавающей запятой: 1391 == 1.0 #=> true 1401 === 1.0 #=> false 141 142# Elixir позволяет сравнивать значения разных типов: 1431 < :hello #=> true 144 145# При сравнении разных типов руководствуйтесь следующим правилом: 146# число < атом < ссылка < функция < порт < процесс < кортеж < список < строка 147 148## --------------------------- 149## -- Порядок выполнения 150## --------------------------- 151 152# Условный оператор `if` 153if false do 154 "Вы этого никогда не увидите" 155else 156 "Вы увидите это" 157end 158 159# Противоположный ему условный оператор `unless` 160unless true do 161 "Вы этого никогда не увидите" 162else 163 "Вы увидите это" 164end 165 166# Помните сопоставление с образцом? 167# Многие конструкции в Elixir построены вокруг него. 168 169# `case` позволяет сравнить выражение с несколькими образцами: 170case {:one, :two} do 171 {:four, :five} -> 172 "Этот образец не совпадёт" 173 {:one, x} -> 174 "Этот образец совпадёт и присвоит переменной `x` значение `:two`" 175 _ -> 176 "Этот образец совпадёт с чем угодно" 177end 178 179# Символ `_` называется анонимной переменной. Используйте `_` для значений, 180# которые в текущем выражении вас не интересуют. Например, вам интересна лишь 181# голова списка, а хвост вы желаете проигнорировать: 182[head | _] = [1,2,3] 183head #=> 1 184 185# Для лучшей читаемости вы можете написать: 186[head | _tail] = [:a, :b, :c] 187head #=> :a 188 189# `cond` позволяет проверить сразу несколько условий за раз. 190# Используйте `cond` вместо множественных операторов `if`. 191cond do 192 1 + 1 == 3 -> 193 "Вы меня никогда не увидите" 194 2 * 5 == 12 -> 195 "И меня" 196 1 + 2 == 3 -> 197 "Вы увидите меня" 198end 199 200# Обычно последним условием идёт `true`, которое выполнится, если все предыдущие 201# условия оказались ложны. 202cond do 203 1 + 1 == 3 -> 204 "Вы меня никогда не увидите" 205 2 * 5 == 12 -> 206 "И меня" 207 true -> 208 "Вы увидите меня (по сути, это `else`)" 209end 210 211# Обработка ошибок происходит в блоках `try/catch`. 212# Elixir также поддерживает блок `after`, который выполнится в любом случае. 213try do 214 throw(:hello) 215catch 216 message -> "Поймана ошибка с сообщением #{message}." 217after 218 IO.puts("Я выполнюсь всегда") 219end 220#=> Я выполнюсь всегда 221# "Поймана ошибка с сообщением hello." 222 223## --------------------------- 224## -- Модули и функции 225## --------------------------- 226 227# Анонимные функции (обратите внимание на точку при вызове функции) 228square = fn(x) -> x * x end 229square.(5) #=> 25 230 231# Анонимные функции принимают клозы и гарды. 232# 233# Клозы (от англ. clause) — варианты исполнения функции. 234# 235# Гарды (от англ. guard) — охранные выражения, уточняющие сопоставление с 236# образцом в функциях. Гарды следуют после ключевого слова `when`. 237f = fn 238 x, y when x > 0 -> x + y 239 x, y -> x * y 240end 241 242f.(1, 3) #=> 4 243f.(-1, 3) #=> -3 244 245# В Elixir много встроенных функций. 246# Они доступны в текущей области видимости. 247is_number(10) #=> true 248is_list("hello") #=> false 249elem({1,2,3}, 0) #=> 1 250 251# Вы можете объединить несколько функций в модуль. Внутри модуля используйте `def`, 252# чтобы определить свои функции. 253defmodule Math do 254 def sum(a, b) do 255 a + b 256 end 257 258 def square(x) do 259 x * x 260 end 261end 262 263Math.sum(1, 2) #=> 3 264Math.square(3) #=> 9 265 266# Чтобы скомпилировать модуль Math, сохраните его в файле `math.ex` 267# и наберите в терминале: `elixirc math.ex` 268 269defmodule PrivateMath do 270 # Публичные функции начинаются с `def` и доступны из других модулей. 271 def sum(a, b) do 272 do_sum(a, b) 273 end 274 275 # Приватные функции начинаются с `defp` и доступны только внутри своего модуля. 276 defp do_sum(a, b) do 277 a + b 278 end 279end 280 281PrivateMath.sum(1, 2) #=> 3 282PrivateMath.do_sum(1, 2) #=> ** (UndefinedFunctionError) 283 284# Функции внутри модуля тоже принимают клозы и гарды 285defmodule Geometry do 286 def area({:rectangle, w, h}) do 287 w * h 288 end 289 290 def area({:circle, r}) when is_number(r) do 291 3.14 * r * r 292 end 293end 294 295Geometry.area({:rectangle, 2, 3}) #=> 6 296Geometry.area({:circle, 3}) #=> 28.25999999999999801048 297Geometry.area({:circle, "not_a_number"}) #=> ** (FunctionClauseError) 298 299# Из-за неизменяемых переменных в Elixir важную роль играет рекурсия 300defmodule Recursion do 301 def sum_list([head | tail], acc) do 302 sum_list(tail, acc + head) 303 end 304 305 def sum_list([], acc) do 306 acc 307 end 308end 309 310Recursion.sum_list([1,2,3], 0) #=> 6 311 312# Модули в Elixir поддерживают атрибуты. 313# Атрибуты бывают как встроенные, так и ваши собственные. 314defmodule MyMod do 315 @moduledoc """ 316 Это встроенный атрибут 317 """ 318 319 @my_data 100 # А это ваш атрибут 320 IO.inspect(@my_data) #=> 100 321end 322 323# Одна из фишек языка — оператор `|>` 324# Он передаёт выражение слева в качестве первого аргумента функции справа: 325Range.new(1,10) 326|> Enum.map(fn x -> x * x end) 327|> Enum.filter(fn x -> rem(x, 2) == 0 end) 328#=> [4, 16, 36, 64, 100] 329 330## --------------------------- 331## -- Структуры и исключения 332## --------------------------- 333 334# Структуры — это расширения поверх карт, привносящие в Elixir значения по 335# умолчанию, проверки на этапе компиляции и полиморфизм. 336defmodule Person do 337 defstruct name: nil, age: 0, height: 0 338end 339 340joe_info = %Person{ name: "Joe", age: 30, height: 180 } 341#=> %Person{age: 30, height: 180, name: "Joe"} 342 343# Доступ к полю структуры 344joe_info.name #=> "Joe" 345 346# Обновление поля структуры 347older_joe_info = %{ joe_info | age: 31 } 348#=> %Person{age: 31, height: 180, name: "Joe"} 349 350# Блок `try` с ключевым словом `rescue` используется для обработки исключений 351try do 352 raise "какая-то ошибка" 353rescue 354 RuntimeError -> "перехвачена ошибка рантайма" 355 _error -> "перехват любой другой ошибки" 356end 357#=> "перехвачена ошибка рантайма" 358 359# У каждого исключения есть сообщение 360try do 361 raise "какая-то ошибка" 362rescue 363 x in [RuntimeError] -> 364 x.message 365end 366#=> "какая-то ошибка" 367 368## --------------------------- 369## -- Параллелизм 370## --------------------------- 371 372# Параллелизм в Elixir построен на модели акторов. Для написания 373# параллельной программы нам понадобятся три вещи: 374# 1. Создание процессов 375# 2. Отправка сообщений 376# 3. Приём сообщений 377 378# Новый процесс создаётся функцией `spawn`, которая принимает функцию 379# в качестве аргумента. 380f = fn -> 2 * 2 end #=> #Function<erl_eval.20.80484245> 381spawn(f) #=> #PID<0.40.0> 382 383# `spawn` возвращает идентификатор процесса (англ. process identifier, PID). 384# Вы можете использовать PID для отправки сообщений этому процессу. Сообщения 385# отправляются через оператор `send`. А для приёма сообщений используется 386# механизм `receive`: 387 388# Блок `receive do` ждёт сообщений и обработает их, как только получит. Блок 389# `receive do` обработает лишь одно полученное сообщение. Чтобы обработать 390# несколько сообщений, функция, содержащая блок `receive do`, должна рекурсивно 391# вызывать себя. 392 393defmodule Geometry do 394 def area_loop do 395 receive do 396 {:rectangle, w, h} -> 397 IO.puts("Площадь = #{w * h}") 398 area_loop() 399 {:circle, r} -> 400 IO.puts("Площадь = #{3.14 * r * r}") 401 area_loop() 402 end 403 end 404end 405 406# Скомпилируйте модуль и создайте процесс 407pid = spawn(fn -> Geometry.area_loop() end) #=> #PID<0.40.0> 408# Альтернативно 409pid = spawn(Geometry, :area_loop, []) 410 411# Отправьте сообщение процессу 412send pid, {:rectangle, 2, 3} 413#=> Площадь = 6 414# {:rectangle,2,3} 415 416send pid, {:circle, 2} 417#=> Площадь = 12.56 418# {:circle,2} 419 420# Кстати, интерактивная консоль — это тоже процесс. 421# Чтобы узнать текущий PID, воспользуйтесь встроенной функцией `self` 422self() #=> #PID<0.27.0> 423 424## --------------------------- 425## -- Агенты 426## --------------------------- 427 428# Агент — это процесс, который следит за некоторым изменяющимся значением. 429 430# Создайте агента через `Agent.start_link`, передав ему функцию. 431# Начальным состоянием агента будет значение, которое эта функция возвращает. 432{ok, my_agent} = Agent.start_link(fn -> ["красный", "зелёный"] end) 433 434# `Agent.get` принимает имя агента и анонимную функцию `fn`, которой будет 435# передано текущее состояние агента. В результате вы получите то, что вернёт 436# анонимная функция. 437Agent.get(my_agent, fn colors -> colors end) #=> ["красный", "зелёный"] 438 439# Похожим образом вы можете обновить состояние агента 440Agent.update(my_agent, fn colors -> ["синий" | colors] end)

Ссылки