Что больше всего бросилось в глаза заядлому рубисту, когда он только только начал изучать Elixir с Phoenix-ом.
Примечание
Я человек простой и глубоко лезть не буду. Посему, будут различия рабоче-крестьянского уровня, а про разницу на уровне запуска приложения, про принципы работы виртуальной машины Erlang'а и протокол OTP ничего сказано не будет.
Главное впечатление
Elixir/Phoenix очень похож на Rails и одновременно совсем не похож на него. Как некоторые английские фразы: по отдельности слова знакомые, а вместе — непонятно.
Erlang vs Ruby
Думать на руби и пытаться писать на эликсире — это тяжело. Регулярно заходишь в тупики, ибо то что ты хочешь делается совсем не так, как привык делать… либо, на самом деле, ты этого вообще не этого хочешь.
А в остальном, про различия Erlang и Ruby люди книги пишут, поэтому буду краток. Для меня главные засады были с заменой рельсовых "паровозов" на пайпы, с переориентированием inject
/foldr
) и с, субъективно, более строгими требованиями к типам данных (хотя, официально, оба языка со строгой динамической типизацией).
Паттерн-матчинг никакого удивления не вызвал и я так и не понял, почему про него столько разговоров. Просто интересный инструмент.
Общий скоуп
В Эликсире всё лежит в модулях. Никакого глобального скоупа. Навевает C#.
Иными словами: рельса плоская донéльзя и местами мешает сделать иерархию (помню были когда-то баги с контроллерами, лежащими в модулях). Эликсир — наоборот, всё по модулям. В рельсе назначение объекта угадываешь по родительскому классу, а в эликсире — по полному названию класса/модуля.
Компилируемость
С одной стороны — это то, чего мне иногда не хватало в рельсе. Так как можно найти добрую половину ошибок прямо при компиляции, а не в рантайме на продакшене. С другой стороны, на компиляцию нужно время. Но с третей стороны, его нужно немного, а больших проектов на эликсире я пока не видел (да и не по заветам эрланга писать большие монолиты). В довершении, ребята из эликсира отлично поработали над динамической перезагрузкой кода и страницы. И пока что, скорость работы вкупе с отсутствием богомерзких zeus/spring мне греет душу.
Конечно же это и минусы порождает, но они вылезают сильно позже. Где-то в районе продакшн окружения и деплоя. Об этом будет ниже.
Тут же интересный момент, который физически не может случиться в рельсе: миграции и прочие штуки, которые в rails через rake
, в elixir требуют компиляции проекта и может случиться что-нибудь вроде: забыл написать роуты, на них ссылается path-хелпер во вьюхе, а отвалились миграции. Поначалу — дико непривычно.
Документация
Сайт с документацией эликсира выглядит гораздо бодрее рубидока и апидока. Но вот объём документации и примеры — это то, в чём ruby/rails далеко впереди. В Elixir сильно не хватает примеров на всё, что чуть сложнее табуретки. Да и описание некоторых методов, по сути, не ушло дальше сигнатуры. Мне, как приученному рубями к обилию примеров и описаний, было сложно с некоторыми методами эликсира. Иной раз приходилось долго тыкаться и экспериментировать, чтобы понять как пользоваться тем или иным методом, ибо язык знаю не так хорошо, чтобы свободно читать исходники пакетов.
Независимость расположения файла от его содержимого
Как говорится "with great power comes great responsibility". С одной стороны можно натворить вакханалию и разложить объекты так, что враг точно не пройдёт. А с другой стороны можно именовать пути более логично и наглядно, добавляя логические уровни директорий, которых нет в иерархии классов. В частности, можно вспомнить trailblazer и ему подобных с идеей объединения всего, что связано с экшеном, в одном месте. В эликсире это можно сделать без сторонних библиотек и кучи классов просто правильно переложив существующие файлы.
Прозрачный путь запроса
Если в Rails вопрос про rack — это непременный атрибут любого собеседования, ибо рельса — это верхушка айсберга и периодически хочется сделать свой middleware. То в эликсире такого желания не возникает совсем (хотя может я ещё молод и всё впереди). Там есть явный набор pipeline, через который проходит запрос. И там явно видно где фетчится сессия, где обрабатывается flash-messge, где csrf валидируется и всем этим можно управлять как вздумается в одном месте. В рельсе всё это хозяйство частично прибито гвоздями, а частично разбросано по разным местам.
Роуты наизнанку
В Rails, ситуация когда один экшн может отвечать в нескольких форматах — это норма. Там даже (.:format)
заложен прямо в роуты. В эликсире, из-за вышеозначенного свойства с pipeline, мысли об аналоге format вообще не появляется. Разные форматы идут по разным pipeline и имеют разные url'ы. По мне так это здо́рово.
Схема в модели
Это вообще сказка. Как опишешь поля модели, так и будет. Никакого неявного каста типов. Плюс нет костылей, чтобы запретить доступ к полю, которе есть в БД, но его по каким-либо причинам нельзя использовать в веб-приложении.
Валидации и колбэки
В эликсире нет колбэков. Там всё более прямолинейно. И, кажется, мне это нравится.
Вместо rails-way в эликсире changeset, который совмещает в себе strong_parameters, валидации и немного колбэков. А остатки колбэков идут через Multi, который даёт возможность набрать кучу операций, транзакционно их выполнить и обработать результат.
Короче, всё просто по-другому. Сначала это непривычно. Потом местами дико бесит, ибо нельзя просто ещё один колбэк для всего воткнуть и не думать о разных бизнес-кейсах. А потом начинаешь замечать "необъянимую прелесть", ибо приходится делать правильно, а не как привык.
Работа с БД
Вместо ActiveRecord появился некие Ecto.Repo, Ecto.Query и ещё несколько их собратьев. Рассказывать все отличия — это отдельная статья получится. Поэтому скажу основные субъективные ощущения.
В дебаге удобнее AR. Так как там общий скоуп, константы из load path подгружаются при обращении к ним и можно просто открыть rails c
, написать User.where(email: 'Kane@nod.tb').order(:id).first
и получить результат.
В Elixir'е консоли недостаточно. Нужно сделать ряд действий:
- заимпортить метод для построения sql-запроса:
import Ecto.Query, only: [from: 2]
; - заалиасить классы, чтобы не писать через точку их полные названия:
alias MyLongApplicationName.User
— чтобы вместоMyLongApplicationName.User
писать простоUser
;alias MyLongApplicationName.Repo
— аналогично для обращения к классу, умеющему выполнять sql и отдавать результаты;
- и только теперь можно написать
from(u in User, where: u.email == "Kane@nod.tb") |> Repo.one
С другой стороны, в коде приложения эти "формальности" дают более читаемый код, плюс есть ощущение, что ты контролируешь происходящее, а не оно живёт своей жизнью. То есть ты сам выбираешь какие методы, модели и другие объекты, нужны для работы, явно их подгружаешь и пользуешься.
Название приложения
По образу и подобию Rails я полагал, что имя приложения используется в паре конфигов и всё. Поэтому на длину названия внимания не обратил. А зря. В Elixir модуль с названием приложения — это верхний уровень в иерархии модулей веб-приложение и он будет фигурировать везде.
Я вот назвал свою песочницу Comindivion. И теперь немного страдаю, так как это довольно длинное название и писать его нужно постоянно. Как в файлах классов, так и в консоли при вызове чего угодно. Кстати да, кому интересно, вот песочница на GitHub.
N+1
В Rails мы её имеем "из коробки", а в Elixir "из коробки" такой проблемы нет. Там на этапе сборки запроса можно указать какие реляции понадобятся и они будут загружены в ходе выполнения этого самого запроса. Не загрузил? Не будет у тебя доступа к этой реляции. Всё просто и красиво.
Обработка запроса и ответ на него
Если коротко: в фениксе всё более явно, нежели в рельсе.
Везде conn
Так как состояние не хранится в куче разных объектов, его приходится таскать за собой в одном объекте. Напоминает request
из ActionController
, только более всеобъемлющий. Зовётся он в Фениксе connection
. Содержит вообще всё: и request
, и flash
, и session
и всё всё всё. Он же фигурирует в вызове всего, что связано с обработкой пришедшего запроса.
Тут и минусы, так как попервой очень лениво лепить везде conn
и не до конца понимать зачем. Рельса в этом плане развращает. Ты пишешь render или flash и нет мыслей о том, что это действие с соединением. А в Phoenix conn
постонно напоминает о работе с конкретным соединением или сокетом, а не просто методы вызываются и там внутри магия происходит.
Partial&template
В Фениксе нет разделения на partial и template. В конечном итоге всё функция. Тут же кроется ещё одна прелесть: рельса даже в прод окружении постоянно лезет за вьюшками на диск и порождает IO плюс оверхед на их преобразование из erb/haml/etc в html. А в Elixir всё функция, и вьюшки в том числе. Скомпилили вьюшку разок и всё: получает аргументы, выплёвывает html, на диск не ходит.
Views
В Rails под view понимают партиал и темплейты, а в Phoenix они лежат в templates, а во views, грубо говоря, обитают разные способы представления данных. В частности, там лежат "переопределения" render'а.
То есть, по умолчанию, контроллер ничего не рендерит. Всё вызывается явно. А если у вас нет партиала и он вам особо не нужен (например в случае с json, когда он легко билдится сервисным классом), вы переопределяете рендер как-нибудь так:
def render("show.json", %{groups: groups}) do
%{
groups: groups
}
end
И партиал больше не нужен.
Heplers
В Phoenix их нет. И это круто! Ибо в рельсовых хелперах, обычно, собирается всякий хлам, который либо лениво было распихивать по углам, либо просто нужно было быстренько чего-нибудь накодить.
Однако, методы в контроллер, представления и тд. добавлять можно. Делается это в специальном месте web/web.ex
и выглядит довольно прилично.
Статика
В девелопменте всё как обычно, разве что в фениксе ещё прикрутили live reload, попервой вызывающий "Уау!" эффект. Это когда поменял css, вернулся в браузер, а там изменения уже сами подгрузились.
В продакшене в Phoenix поведение статики немного иное, нежели у рельсы. По-умолчанию, явно прописаны места, откуда можно тащить статику и нельзя просто так добавить файликов в ассеты, чтобы раздавать их. Ещё есть маппинг дефолтных ассетов, чтобы лишний раз по ФС не блужлать, а сразу брать нужный файл и отдавать его.
Ассеты
Из коробки в Фениксе — brunch. Можно заменить на webpack. Но есть довольно правдивая шутка про то, что многие проекты загибаются на этапе настройки webpack'а.
Короче, js и css более-менее собираются, а вот с остальной статикой в бранче не очень. Её либо копипастирь руками прямо в проект из node_modules (мне этот вариант совсем не нравится), либо писать хуки на баше. Например, так.
Работа с SSL
"Из коробки" в Фениксе идёт маленький http-сервер, называемый cowboy. С виду напоминает рубийную пуму. У них даже количество звёздочек на GitHub примерно одинаковое. Но как-то мне не зашла настройка SSL ни в одном из вышеозначенных. Особенно вместе с Let's Encrypt, доп.файлом конфига веб-сервера и регулярным обновлением сертификата. Так что как http-сервер — ок, а для ssl беру прокси на localhost через apache/nginx.
Деплой
Он вообще другой, по сравнению с рельсой. В Rails, в минимальном варианте, склонил репку на сервер, поплясал с бубном для бандла, конфигов, ассетов и запустил приложение. А эликсир же компилится и закопать трамвай склонить репку не прокатит. Нужно собирать пакет. И тут начинается:
- узнаёшь зачем нужен applications в
mix.exs
, ибо без правильно их указания в проде чудесные ошибки; - узнаёшь, что переменные окружения вкомпиливаются на моменте сборки пакета, а не на момент его запуска и это в первые разы вызывает дикое удивление; потом узнаёшь про relx вместе с
RELX_REPLACE_OS_VARS=true
и немного отпускает; - удивляешься, что в собранном пакете для продакшена нет ничего похожего на rake, в частности нет миграций и их нужно как-то отдельно запускать, например, из дев.окружения через проброс порта к БД (или через eDeliver, который сделает примерно то же самое).
А потом, как с вышеописанным разберёшься, начинаются плюсы:
- можно сделать пакет самодостаточным и на боевой машине вообще ничего не ставить из зависимостей; просто tarball распаковать и запустить содержимое; разве что erlang раскатать может понадобиться, так как его cross compile вариант немного нетривиальнен в сборке;
- можно делать upgrade release, чтобы деплоить без downtime.
Дебаг
В Elixir есть Pry и работает аналогично рубям. Даже есть аналог rails c
, выглядящий как iex -S mix
.
Но в продакшене консолью пользоваться приходится иначе, так как пакет собран и mix
в нём нет. Приходится подключаться к работающему процессу. Это радикально отличается от рельсы и в начале тратишь много времени на гуглинг способа запуска эликсир-консоли в продакшене, ибо ищешь что-то аналогичное рельсе. В итоге понимаешь что делать всё нужно иначе и вызываешь что-то вроде: iex --name trace@127.0.0.1 --cookie 'from_env' --remsh 'my_app_name@127.0.0.1'
.
Продолжение следует...
Фух, по-любому что-нибудь забыл. Ну да ладно. Лучше вы рассказывайте, что вас удивило в Elixir, в сравнении с другими языками.
Автор: Loriowar