Elixir — мова для роботи з I/O. Синтаксис, документація та спільнота
Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті
Мова для роботи з I/O: саме так описує Elixir José Valim — автор мови програмування.
Elixir — відносно молода мова, її розробка почалася у 2011 році, а перша версія була вже у 2014. Про Elixir я почув вперше у 2016 році від співробітника. Зовсім незабаром, José Valim виступив у Львові на Pivorak meetup з доповіддю Phoenix Framework for the new web. От якраз перед цією доповіддю я познайомився з мовою та середовищем ближче. Ну так, в загальному, щоб розуміти максимум з доповіді :D І мені там це продали — 2 мільйони websocket з’єднань на одній машині, можливість бачити μs в логах, візуальна схожість з Ruby, функційне програмування. Під кінець року мені вже вдалося влипнути в написання продакшн коду на Elixir.
От, все, що ви чули файне про Erlang, про високу конкурентність, високодоступні системи, толерантність до відмов і т.д., наслідує й Elixir, власне як і будь-яка інша мова на Erlang VM. Але Elixir акцентує додатково увагу на тих речах, де Erlang кульгає — на активній спільноті, на доступності для новачків та на DX (Developer Experience).
Я б не хотів копіювати базову інформацію з Вікіпедії, тому просто раджу швидко ознайомитись самотужки Elixir (programming language), там лаконічно описано про основні фічі мови та є приклади коду. Натомість я опишу особливості, а відносити їх до плюсів чи до мінусів — вирішуйте самі.
Синтаксис
На жаль, в сучасному світі синтаксис мови програмування відіграє більшу роль, ніж цього хотілося б. Люди легше засвоюють речі, які подібні до того, що вже відомо і цей процес є закономірним. Візуальна схожість з Ruby притягує програмістів: згідно з опитуванням Elixir Survey 2020, близько половини людей прийшли з Ruby-спільноти. Це свого роду один з пунктів продажу мови: Elixir — то як Ruby, тільки трішки інший. Є шматки коду, де Elixir і Ruby не відрізниш, але є ще більше, де навпаки. Після детальнішого знайомства все стає на свої місця: Elixir — це Erlang, поданий під іншим соусом.
Багатьом людям дуже не подобається синтаксис Erlang, який бере своє коріння з Prolog. Зрештою, перша версія Erlang була на ньому написана.
José Valim хотів мати Ruby на віртуальній машині Erlang і у своїх перших версіях Elixir була об’єктно орієнтованою мовою і не працювала нормально. З того часу мова суттєво еволюціонувала, ось маленький приклад з викликом функції map:
% перший концепт Elixir [1, 2, 3].map do (x) x * 2 end
# Ruby [1, 2, 3].map do |x| x * 2 end
# сучасний Elixir Enum.map([1,2,3], fn x -> x * 2 end)
% Erlang lists:map(fun(X) -> X * 2 end, [1,2,3]).
Можна відкрити старий readme і подивитися більше прикладів синтаксису з ідеями того часу. Ось тут є проміжний дизайн мови, яка стала наріжним каменем того, що ми маємо зараз.
Віртуальна машина та середовище виконання
Elixir як мова, мабуть, не була б такою привабливою, якби не BEAM. Це наразі основна імплементація віртуальної машини для Erlang, яка підтримується компанією Ericsson.
Чи не головною особливістю Erlang є те, що він розроблявся для проєктування систем реального часу, через це планувальник задач має змогу використовувати витискальну систему багатозадачності (preemptive multitasking). Це означає, що планувальник не буде чекати, поки процес добровільно погодиться на зміну контексту, а зробить це примусово по лічильнику редукцій (викликів функцій). Тобто навіть коли процесор повністю навантажений по всіх ядрах CPU-інтенсивними без I/O задачами, планувальник завжди знайде місце для ще одного маленького HTTP-запита на 5 мілісекунд і не доведеться йому чекати в черзі, поки інші завдання звільнять ресурси. Функційне програмування в Erlang — це практична необхідність, яка дозволяє реалізувати такий підхід.
Інструменти для зневаджування — це ще одна з сильних сторін в Erlang. Є можливість досить легко під’єднатися до запущеної машини в продакшені, знайти процес віртуальної машини, подивитися його стан та подивитися які виклики функцій він зараз робить. Насправді можна багато чого іншого ще там дізнатися та є досить багато інструментів для цього. Вас це зовсім не вражає? А от мене дуже вразило :) Особливо, коли знадобилося на практиці вперше. На одному з проєктів, де я брав участь, була ситуація, коли після 10 хвилин сесії вона в рідкісних випадках блокувалась — це було на вебсокетах і нові події з бекенду просто не приходили. Я під’єднався до машини, побачив, що процес, який відповідає за сесію живий, його стан не міняється, він щось там собі рахує, подивився що конкретно. Проблема була в такій функції:
left_rotate([1, 2, 3], 2) # [3, 1, 2] left_rotate([1, 2, 3], 0) # [1, 2, 3]
вона відкушує голову списку і кладе в кінець стільки разів, допоки другий аргумент не зменшиться до 0. Ну і вгадайте що пішло не так? Після рефакторингу в другий елемент прилетіло від’ємне значення ¯\_(ツ)_/¯. Оскільки я в той день вже не мав змоги над тим працювати, то я спокійно собі продовжив роботу на наступний — нічого за той час не лягло, оперативка не закінчилась, не було ніяких інших проблем в сусідніх процесів, процес далі жив, проєкт працював. Я не звик до такої стабільності!
Робив я ще якось запити на внутрішній сервіс, написаний на мові X, з Elixir — це було звичайне завантаження файлів, але я якось незвично склав запит, мідлвара X не могла розібрати дані і сервер X стабільно злягав. Для мене це було несподіванкою. Я знав, що таке може бути, але не очікував, що в мене! Ну вже мали б ту бібліотеку на декілька тисяч зірочок вилизати! Я, звичайно, не зробив патч туди, позаяк був захоплений обсиранням X серед співробітників. Девопс все одно кубернетісами то обклав, недарма ж йому гроші платять. В роботі з BEAM такий випадок неможливий, треба навмисне написати щось типу кривого NIF, щоб система злягла.
Що я хотів цим сказати? Що стійкість до відмов як фундаментальна особливість надзвичайно важлива для хорошого сну.
Ще розвивається альтернатива — lumen — все як має бути — на Rust і щоб можна було в браузері запускати — вже навіть хеллоу ворлди працюють.
Модель акторів
Суперлегка модель для розуміння та використання, принаймні зараз так здається.
Пишеш синхронний блокувальний код і не хвилюєшся як ця функція має викликатися. Не треба бавитися в callback-hell, не треба в коді зазначати, що якась функція є особливою, позначаючи її ключовим словом async. Немає основного треду, блокування якого треба уникати. Процесів є багато і вони спілкуються між собою повідомленнями асинхронно.
Не раз чув як пітоністи в сусідній кімнаті обговорювали асинхронний код, наче якесь таїнство, для якого треба особливі навички. На Elixir проєкті в нас таких розмов навіть немає — ці проблеми вже вирішені за нас і ми займаємось більш високорівневими задачами. Високорівневою мовою програмування користуємось, як-не-як.
Документація і стандартна бібліотека
Окрім того, що на документацію приємно дивитися естетично hexdocs.pm/elixir/Kernel.html, вона в Elixir спільноті вважається не менш важливою ніж код, тому PR з покращеннями охоче вітаються. Документацію чи не до всіх бібліотек можна знайти на https://hexdocs.pm, вона не розкидана по wiki сторінкам на github, readme та самопальних вебсайтах. Останні не є мінусом, коли головне щоб хоч якась документація була, але стандартизація для таких речей це зручно.
Стандартна бібліотека не перевантажена, але і не вбога. Наприклад, вам не треба додавати додаткову залежність чи копіювати реалізацію функції Enum.uniq/1 з StackOverflow, не треба на String.starts_with?/2 чекати восьмої версії мови. Але тут немає JSON в стандартній бібліотеці як це є в Ruby, немає вбудованого модуля Oauth як це є в Crystal. Для цього потрібні сторонні рішення. Водночас величезний багаж різних модулів та функцій, поставляється відразу в Erlang/OTP, тому модуль для роботи з zip, для прикладу, все-таки доступний відразу. Варто зауважити, що для роботи з Elixir знання Erlang не вимагаються, а коли доведеться працювати з якимись Erlang модулями, то розібратися можна надзвичайно швидко, через те, що Erlang теж функційна мова і там різниця буквально лише в синтаксисі. Та і не треба ментально перемикатися на іншу парадигму. Clojure, на противагу, повинна вживатися якось в об’єктноорієнтовану екосистему JVM.
Функційне програмування
Так, ось тут воно є і тільки воно*. Однак José не рекомендує продавати мову як ФП, це не основна фішка.
Варто лиш зауважити, що ФП в Elixir це не те саме, що ФП в Haskell. Тут не обов’язково знати що таке аплікативний функтор та монада-трансформер. В усе це можна вдаритись, але сенсу особливого тут немає, через те, що йде всупереч тому, як пише вся спільнота, до того ж мова динамічно типізована.
В Elixir, як і в багатьох інших функційних мовах, для контролю потоку не використовують Exception Driven Development — помилки заведено обробляти як звичайні значення. Для зручного компонування таких виразів є конструкція with, її можна вважати певним аналогом do-нотації в Haskell.
* щоб накинути, процитую Alan Kay:
OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things.
Відповідно, можемо вважати Elixir справжньою ОО мовою, на відміну від Java та інших ;) Персонально мені здається, що написання пачки довгоживучих GenServer’ів в одному проєкті мені дало більше розуміння ООП шаблонів, ніж робота з Ruby протягом кількох років.
Метапрограмування
Якщо віртуальну машину можна вважати хлібом, то метапрограмування в Elixir то є масло. Я люблю метапрограмування, правда не так писати, як використовувати гарно спроєктовані інструменти з його допомогою. Elixir має потужну та зручну систему для метапрограмування, на яку суттєво вплинула Clojure.
Одним з моїх улюблених прикладів є стандартний фреймворк для тестування ExUnit. Візьмемо приклад коду з документації
defmodule StringTest do use ExUnit.Case, async: true describe "String.capitalize/1" do test "first grapheme is in uppercase" do assert String.capitalize("hello") == "Hello" end test "converts remaining graphemes to lowercase" do assert String.capitalize("HELLO") == "Hello" end end end
Те, що блоки describe i test є макросами — то ясно як божий день. Я б хотів акцентувати увагу на assert — цей вираз складніший, ніж здається на перший погляд. Складність ховається у звітуванні про помилки, коли умова не справджується і тест завершується невдало. assert аналізує не просто кінцеве булеве значення, а весь вираз. Змінимо перший тест на таке:
assert String.capitalize("hello") == "Hell Yeah"
запустивши який, ми отримаємо наступний звіт:
Саме так, підсвітка різниці між лівою та правою частинами! Звичайно, це буде працювати не тільки з рядками, а й з іншими структурами даних.
Ось ще приклад із зіставленням зі зразком (pattern matching):
data = %{user: %{first_name: "John", age: 18}} assert {:ok, %{age: 13}} = Map.fetch(data, :user)
результат:
Ну це є у всіх нормальних фреймворках, ви скажете, і це так, але для подібного функціоналу там є достобіса різних хелперів з різними закінченнями та префіксами. І все для того, щоб нормально описати що ж відбувається! А тут просто додаєте assert до звичних виразів. Я відчуваю когнітивний розслабон при написанні тесту assert 1 < 3
замість expect(1).toBeLessThan(3);
(JS), assert 3 in 1..5
замість expect(3).to be_between(1, 5)
(ruby), таких прикладів можна придумати безліч, думаю, ідею ви зрозуміли.
Гріх тут і не згадати про бібліотеку Ecto, позаяк чималій кількості Elixir розробників доведеться з нею працювати. LINQ з .NET суттєво вплинув на мову запитів, але через те, що Elixir має підтримку макросів, то щоб підтримувати такий функціонал не потрібно додаткових інтеграцій в мову, на відміну від C#. Ось не зовсім вже примітивна вибірка з бази даних з використанням Ecto:
query = from employees in Employee, select: %{ employee: employees.name, salary: employees.salary, avg_salary_in_department: employees.salary |> avg() |> over(partition_by: employees.depname) }, inner_join: companies in assoc(employees, :company), where: companies.size == ^:big, where: not is_nil(employees.updated_at) or employees.inserted_at > ago(3, "month"), order_by: employees.name Repo.all(query)
Коли ознайомлювався з Ecto, то тішився як слон можливості написати запит з or і з дужками, типу (A or B) and (C or D), не кажучи вже про window функції та CTE. Звичайно, є випадки, коли доведеться написати сирий SQL, але ця межа зсунута досить далеко.
Наслідування та ось це все, що сьогодні йменують як ООП є у вигляді сторонньої бібліотеки oop, яку зробив один з учасників core команди Elixir.
import OOP class Animal do var :name end class Dog < Animal do var :breed end snuffles = Dog.new(name: "Snuffles", breed: "Shih Tzu") snuffles.name # => "Snuffles"
Звичайно, то є просто забавка на макросах і ніхто цю бібліотеку не використовує.
Web фреймворк Phoenix дуже і дуже швидко віддає користувачеві результат через використання стандартної системи шаблонування EEx і власного рушія, який працює з iodata. Якщо коротко, то iodata — це якісь дані, які можна вивести в I/O, при цьому при виводі списку він робиться плоским:
> IO.puts(["a", ["b", ["c"]], [], "d"]) abcd :ok > IO.puts(["a", ["b", ["c"]], [], "d"] |> List.flatten() |> Enum.join()) abcd :ok
Обидва виклики IO.puts виводять однаковий результат, але в другому варіанті є але — це оверхед у вигляді конкатенації рядків, через те що віртуальна машина буде у пам’яті мати окремо виділене місце під кожен елемент списку + конкатенований результат. Phoenix.HTML.Engine використовує цю особливість для оптимізацій виводу уникаючи зайвої конкатенації.
Нехай у нас є ось такий шаблон:
<div> <%= user_name <- @user_names do %> <div><%= user_name %></div> <% end %> </div>
теги тут є статичними елементами, відповідно, з цього шаблона під капотом генерується функція з приблизно ось таким вмістом:
iex> template |> EEx.compile_string(engine: Phoenix.HTML.Engine) |> Macro.to_string() |> IO.puts() ( arg0 = case(user_name <- @user_names do arg0 = case(user_name) do {:safe, data} -> data bin when is_binary(bin) -> Plug.HTML.html_escape_to_iodata(bin) other -> Phoenix.HTML.Safe.to_iodata(other) end {:safe, ["\n <div>", arg0, "</div>\n "]} end) do {:safe, data} -> data bin when is_binary(bin) -> Plug.HTML.html_escape_to_iodata(bin) other -> Phoenix.HTML.Safe.to_iodata(other) end {:safe, ["<div>\n ", arg0, "\n</div>\n"]} )
Рядки з тегами віртуальна машина видає постійно з тих самих комірок пам’яті, таким чином ми маємо автоматичне кешування статичних елементів.
Спільнота
Навіть не знаю як словами то краще описати. Спільнота надзвичайно дружня та доброзичлива. Культура спілкування на дуже високому рівні.
На StackOverflow є якась активність, але я, як і більшість, зависаю на форумі https://elixirforum.com, та і гугл якось частіше туди відправляє, як мені здається. Можливо, саме тому Elixir цього року вже не потрапив до Stack Overflow Developer Survey 2020 :D. Ще є простір в Slack, >29 тисяч учасників в #general каналі на момент написання статті. Взагалі активність в Slack має суттєвий негативний фактор — історія повідомлень не зберігається і по ній ніяка пошукова система не робить індексацію, відповідно знання втрачаються в тому потоці повідомлень.
Готові Elixir розробники на ринку, звичайно, є, але працедавцю треба бути готовим до розвитку Elixir розробників/розробниць самотужки або до співпраці з іноземними спеціалістами. На щастя це є легка мова (куди легша ніж ruby, javascript та купки інших) і більшість програмістів зможе писати на ній досить швидко, головне мотивація :).
Elixir та Erlang існують в симбіозі — Elixir приносить свіжу кров в екосистему, а Erlang дає хороший фундамент для розвитку.
Динамічна типізація
Значним мінусом для багатьох є відсутність статичної типізації. Хоч Elixir і динамічно типізована мова, але все одно має чимало статичних перевірок на етапі компіляції. Є ще dialyzer, але він дуже повільний і часто від нього толку як від козла молока. Є інші мови на BEAM, типу alpaca, gleam, elmchecmy, purerl (erlang backend для purescript), hamler (форкнутий purescript командою EMQ X), erlang backend для idris 2, але їх поширеність в продакшені прямує до нуля. Нещодавно було також анонсовано, що WhatsApp веде розробку типізованого Erlang, обіцяють результати якісь показати в листопаді цього року. Чи це буде мати якийсь вплив на Elixir — невідомо.
Продуктивність на CPU-інтенсивних задачах
Якщо коротко — то вона тут не велика. Коли таке реально знадобиться, то одним зі способів розв’язання таких проблем є NIF (Native Implemented Functions) і якась мова з ручним управлінням пам’яті. Це є надзвичайно поширений спосіб покласти віртуальну машину через свою криворукість. Щоб так не сталося, говорять багато про використання rust.
Час компіляції
Компілятор не може похвалитися надзвичайною швидкістю. Через наявність метапрограмування компіляція займає більше часу, ніж могло б бути без нього. Зовсім не складно відстрілити собі ногу зв’язавши код так, що при зміні одного дрібного файлу буде ще сотня залежних перекомпільовуватись. Але це не складно виправляється і є інструменти для інспекції залежностей такого роду.
У версії 1.11 (остання стабільна) суттєво покращили це питання. Також в master гілку Erlang/OTP додали JIT-компіляцію. Ці 2 фактори суттєво покращують ситуацію.
В загальному я б схарактеризував швидкодію як «нормальна».
Підтримка редакторами
Елементарна підтримка типу підсвітки коду, відступи, перехід на місце оголошення функції і т.д. звичайно є, а щось просунутіше може ще кульгати. Особисто в мене elixir-ls не стабільно у зв’язці з neovim працює (може то я просто криворукий, але хочеться просто включити і щоб працювало), тому в основному використовую alchemist.vim останній комміт куди був пів майже пів року тому. Плагіни для перевірки синтаксису та показу інших попереджень теж у мене вимкнені, тому що щоб перевірити чи функція існує, треба скомпілювати весь проєкт, а наявність системи макросів робить це потенційно небезпечним, бо ти, типу, хочеш перевірити синтаксис, а там код запускається, генеруються функції + на це ще може накладатися попередній пункт про час компіляції.
Стабільність мови
Зараз Elixir має мажорну версію 1, розробка нових великих фіч не ведеться і наразі немає на них планів. Також немає планів на мажорний Elixir v2 реліз. Чому так? По-перше, в дизайн було інвестовано багато часу і зусиль, щоб не треба було кілька разів переробляти. Elixir має достатньо механізмів, щоб будувати потужну екосистему без необхідності змін безпосередньо в самій мові. По-друге, багато роботи зроблено і досі робиться в Erlang/OTP. Тому Elixir 2 якщо і вийде, то лише з остаточним видаленням API який зараз вже позначений як застарілий. Принаймні зараз така риторика.
49 коментарів
Додати коментар Підписатись на коментаріВідписатись від коментарів