Различия Phoenix и Rails глазами новообращённого

    Что больше всего бросилось в глаза заядлому рубисту, когда он только только начал изучать Elixir с Phoenix-ом.

    Примечание


    Я человек простой и глубоко лезть не буду. Посему, будут различия рабоче-крестьянского уровня, а про разницу на уровне запуска приложения, про принципы работы виртуальной машины Erlang'а и протокол OTP ничего сказано не будет.


    Главное впечатление


    Elixir/Phoenix очень похож на Rails и одновременно совсем не похож на него. Как некоторые английские фразы: по отдельности слова знакомые, а вместе — непонятно.


    Erlang vs Ruby


    Думать на руби и пытаться писать на эликсире — это тяжело. Регулярно заходишь в тупики, ибо то что ты хочешь делается совсем не так, как привык делать… либо, на самом деле, ты этого вообще не этого хочешь.


    А в остальном, про различия Erlang и Ruby люди книги пишут, поэтому буду краток. Для меня главные засады были с заменой рельсовых "паровозов" на пайпы, с переориентированием мышления на функциональщину (благо был старый опыт Haskell'я и общая любовь к 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, в сравнении с другими языками.

    Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

    И вообще, как у вас с Elixir?

    Поделиться публикацией

    Комментарии 16

      0

      А почему все Рубисты как один ломанулись на Elixir?

        +3
        Потому что они издалека увидели нечто напоминающее родной язык. Oh, wait! Это же не оно! Но к этому моменту ловушка уже захлопнулась.
          +2
          Потому, что туда ломанулись RoR-гуру, тот же Хосе Валим.
            0

            Но почему они туда ломанулись?

              +1
              Как рассказывал сам Валим:
              Could you tell us more about it? How did this happen?

              It is a long story, but I will try to make it short and sweet. Back in 2010, I was working on improving Rails performance when working with multi-core systems, as our machines and production systems are shipping with more and more cores. However, the whole experience was quite frustrating as Ruby does not provide the proper tool for solving concurrency problems. That’s when I started to look at other technologies and I eventually fell in love with the Erlang Virtual Machine.

              I started using Erlang more and more and, with experience, I noticed that I was missing some constructs available in many other languages, including functional ones. That’s when I decided to create Elixir, as an attempt to bring different constructs and excellent tooling on top of the Erlang VM.
          +1
          нет миграций и их нужно как-то отдельно запускать, например, из дев.окружения через проброс порта к БД (или через eDeliver, который сделает примерно то же самое).


          Можно и без проброса портов и прочих хаков с помощью Distillery. И рантайм на сервер ставить не придется. habr.com/post/331598 – тут и про миграции и про webpack.
            0

            Про миграции интересно, спасибо. А про рантайм не нашёл. Можете подробнее рассказать? Я всегда мыслил, что при сборке пакета на ОС, отличной от продакшена, рантайм нужен либо cross compile, либо без рантайма и на прод его отдельно ставить… или я ошибаюсь?


            А про Distillery… честно говоря, не пользуюсь им особо. Баш скрипт на 10 строк для деплоя и всё.

              0
              Ну, Distillery больше не про деплой как таковой, а про сборку релизов и пакетирование (если оно есть). Проблем с отличиями ОС особо нет, т.к. собирается все равно приложение на CI, на котором такой же Линукс, как и на проде (и других окружениях). CI постоянно гоняет сборки с тестами для всех веток, деплой тоже делается с помощью него (+ Ansible). Цепочка выглядит как собираем релиз (c ERTS) -> заворачиваем в rpm/deb пакет -> кладем в Artifactory, который подключен как репозиторий на энвах -> на энве ставим из пакета, миграции и прочие скрипты запускаются как пост-хуки системного пакетного менеджера (%post для RPM, например). Ну и интеграция с systemd соотв. Еще можно «запекать» сразу образы, AMI для Амазона того же. Не уверен, конечно, что такая машинерия всем нужна и подходит. У меня какое-то время крутилось свое маленькое приложение (для нужд семьи) на Фениксе — деплоил я его тоже с помощью баша, закидывая архив релиза, полученный с помощью Distillery, на сервак, распаковывая и передергивая systemd.
            +2
            В эликсире нет колбэков.

            Это не совсем верно. В эликсире есть колбэки, например, Enum.map/2. В эликсире нет блоков в том же смысле, в каком они есть в рубях. Зато есть макросы, через которые можно организовать похожее поведение при помощи quote/unquote.

            Так как состояние не хранится в куче разных объектов, его приходится таскать за собой в одном объекте

            И в этом есть определённый смысл, в отличие от всяких там JS, куда функциональщину тянут просто потому, что так модно. Elixir — это конруррентро-ориентированный язык, каждый вызов функции может отрабатывать не только в разных потоках или процессах, но даже на разных машинах. Для связывания всего этого существуют специальные инструменты, которые, собственно, и составляют основную мощь BEAM/OTP.

            Он вообще другой, по сравнению с рельсой.

            Самое интересное начинается тогда, когда пытаешься развернуть в кластере т.н. umbrella application. Это что-то вроде мета-проекта, который является обёрткой над кучей мелких сервисов.

            В Elixir есть Pry и работает аналогично рубям.

            А ещё есть классический дебаггер Erlang, который ко всему прочему показывает деревья процессов.
              0

              До максосов руки ещё не дошли, но уже предчувствую веселье. А вот про амбрелу ни разу ничего хорошего не слышал и даже связываться не охото. Можете что-нибудь более предметное из практики про неё рассказать?

                0
                Для веб-разработки это скорее всего не пригодится. Ну, и вообще, использование Elixir только для веба — стрельба из пушки по воробьям.

                Пример: прокси-граббер. Одна часть собирает ссылки и ставит их в очередь на обработку парсеру списков, парсер ставит адреса в очередь чеккера, который уже перенаправляет проверенные прокси в базу. В дополнение сбоку присобачивается сетевой сканнер и брутфорсер. Это всё отдельные части одного проекта, которые, очевидно, есть смысл размещать на машинах разной конфигурации.
              +2

              Про импорт модулей в iex. Добавьте в корень проекта файлик .iex.exs и в него что-то типа вот этого:


              import Ecto.Query
              alias Opm.Repo
              alias Opm.Origin
              alias Opm.FormField

              Сильно облегчит жизнь в консоли :)

                +1

                У такого файлика есть один маленький недостаток — попытка запустить iex вместо iex -S mix из корня проекта заканчивается CompileError'ом.


                Если вдруг кого-то это так же раздражает как меня, и если такой файлик используется только в корне mix-проектов, то вот баш-скрипт который решает эту проблему небольшим костылем:


                iex() {
                  iex_executable=$(which iex)
                  if [ -f .iex.exs ] && [ "$1" == "-S" ] && [ "$2" == "mix" ]
                  then
                    $iex_executable "$@"
                  else
                    $iex_executable --dot-iex "" "$@"
                  fi
                }
                0
                Спасибо за статью, отличное сравнение. Недавно сам перешел с Rails на Phoenix. Из классного, что вы не упомянули это Context. По дефолту в рельсе бизнес логика размазана по контроллерам и моделям(даже в скаффолде). Потом люди начинают думать головой и выносить все это в сервисы. В эликсире же с версии 1.3 ввели Context которые по факту просто модули с функциями внутри. Это позволяет сразу всю бизнес логику заворачивать в Context и в контроллере просто его вызывать. И самое приятно, что при генерации скаффолдов сразу генерируется правильная структура, а не бизнес логика в контроллере.
                  0

                  Справедливости ради, Context не спасает от бизнес-логики в контроллере, он её умеренно локализует. В крайнем варианте можно не проникнуться и начать использовать один контекст внутри другого. Вот тут веселье начинается… Но в общем случае да — отличная штука.


                  И тут же мне любопытно, кто у кого идею контекстов спёр? Фаулера не предлагать, с ним и так всё понятно. Просто в начале был MVC, потом на него начали накидывать разные сервисные классы, а потом начали появляться trailblazer'ы с аналогами контекста. Они до одной и той же идеи параллельно с эликсиром дошли или одни у других утащили? Или там есть кто-то третий, откуда все вдохновение черпают?

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое