Это беззаботное изложение моего опыта изучения Elixir на основе Ruby.

Все началось в начале прошлого года. Коллега рассказывал об этом языке программирования под названием Elixir и о множестве интересных вещей, которые он делал.

Я никогда не слышал об этом, но это звучало очень захватывающе, поэтому я решил прочитать. В конце концов, за последний год меня привели в познавательное приключение.

Это было моим введением в парадигму функционального программирования. Это научило меня думать о коде по-другому и расширило мои представления о программировании.

Начиная с «начала»

Я всегда предпочитал учиться, когда мне просто бросали проблему. Обучение стало побочным эффектом метода проб и ошибок, направленного на решение проблемы. Вот где я сделал свою первую ошибку.

Я начал с середины. Я установил простое приложение с Phoenix и попробовал вместе взломать небольшое приложение. Ничего не имело смысла, и код вел себя очень странно.

Я был действительно сбит с толку, и хотя после долгих поисков в Google я смог кое-что взломать, у меня было так много вопросов без ответов!

Чувствуя себя немного напуганным всем испытанием, я почувствовал необходимость вернуться к основам. Я хотел понять, что я взламывал вместе и почему мне приходилось делать это определенным образом.

Функциональный что ?!

Решил прочитать книгу Изучение функционального программирования с помощью Эликсира.

Выбирая эту книгу, я обращал больше внимания на слово «Эликсир» в названии, чем на слова «Функциональное программирование». Как я позже узнал, я обращал внимание не на то. Меня сбил с толку не Эликсир - язык. Это была парадигма функционального программирования, которую я не понимал.

Есть одна ключевая концепция функционального программирования, которая однажды пришла мне в голову, и все обрело смысл. Это была концепция неизменяемых переменных.

Неизменяемые переменные

Но почему это так важно? У JavaScript нет const? У Ruby нет .freeze? Во множестве других языков также есть свои эквиваленты неизменяемых переменных или констант, так почему это так важно?

Что ж, Elixir обеспечивает неизменность переменных. Это означает, что в Elixir все переменные неизменяемы. Вы просто можете НЕ изменять переменную после присвоения. Период.

Петли никогда не были прежними

Свойство неизменяемости немедленно выбрасывает for петли и while петли за пределы окна. Их нельзя использовать… ну совсем!

Поскольку условие выхода этих циклов контролируется проверкой состояния изменяющейся переменной (то есть индекса a), если у вас есть неизменяемые переменные, эти циклы просто не будут работать. Индекс не может изменяться! Ничего не мутирует!

Если вы хотите писать петли, вы должны использовать * барабанную дробь * да… Рекурсия.

Здравствуйте, Recursion, мой старый друг
Я пришел поговорить с вами снова
Потому что «For» и «While» ушли
Пожалуйста, не заставляйте меня ломать голову

Из твоей памяти, которая была посажена мне в мозг
Ничего не осталось
С тех пор, как я учился в колледже

Все циклы в Эликсире построены на рекурсии. Модуль Enum выполняет внутреннюю рекурсию. for понимание в Эликсире - это просто синтаксический сахар. Все шлейфы повторно проклинают!

Выбрасывать предметы в окно

В ООП у вас есть конструктор объекта и, возможно, несколько методов установки / получения. Когда у вас есть неизменяемые переменные, объект не может иметь никаких сеттеров или каких-либо функций, которые изменяют его состояние.

Неизменяемые объекты на самом деле могут быть представлены более простым способом, просто используя struct для представления их переменных и функций с разделением имен. Приведенный ниже фрагмент Эликсира является примером.

В Elixir есть оператор конвейера, который передает результат последней функции следующей. Это делает приведенный выше вызов более красивым, например:

Если бы я распечатал результат каждого вызова функции выше, он бы выглядел так:

%Gemstone{color: "muddy", weight: nil} <- after .find
%Gemstone{color: "red", weight: nil}   <- after .clean
%Gemstone{color: "red", weight: 5}     <- after .weigh

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

Функциональная парадигма

Поскольку переменные неизменяемы (или я должен называть их значениями). Ответственность за вычисление вывода на основе ввода лежит исключительно на самих функциях. Таким образом, функция становится основным строительным блоком в функциональном программировании.

выход = mod3 (mod2 (mod1 (вход)))

Это очень похоже на типичное математическое уравнение.

y = p(g(f(x)))

Как только я понял неизменность и ограничения, которые она накладывала на язык, я наконец понял 70% вопросов «почему».

Однако есть некоторые другие особенности этой парадигмы, которые мне также пришлось изучить. Я видел, как некоторые из этих шаблонов были приняты и в Ruby.

Сопоставление с образцом

Хотя в Ruby v2.7 появляется больше сопоставлений с образцом, аспекты сопоставления с образцом уже присутствуют с оператором сопоставления регулярных выражений =~. В частности, захват совпадений в группы захвата или локальные переменные.

Сопоставление с образцом в Эликсире присутствует везде. В некотором смысле это похоже на условное присвоение, сжатое в очень короткое выражение.

Одна интересная функция сопоставления с образцом, которая есть в Elixir, - сопоставление аргументов функции. Например, давайте напишем функцию для сортировки чистого Gemstones.

Имеется 3 sort_clean функций, но арность каждой по-прежнему равна 1. Посредством сопоставления входных аргументов по шаблону программа выбирает по порядку, какую из них выполнять. Подумайте об этом как о написании условных выражений и присвоений переменных, которые обычно существуют внутри одной функции.

Мы можем вызвать функцию сортировки с некоторыми грязными драгоценными камнями, например такими:

Что в конечном итоге вернет чистый драгоценный камень.

[%Gemstone{color: "red", weight: nil}]

Функции высшего порядка

Функции могут быть составлены из других функций. Это похоже на замыкания Рубина, но в Elixir чисто функционально.

Например, функция Enum.map выполняет итерацию по перечислимому значению, но вы можете расширить функциональность итерации, передав другие функции.

Параллелизм и параллелизм

Поскольку при каждом вызове функции Elixir не нужно беспокоиться о непреднамеренном изменении состояния другой функции (из-за неизменности значения), все функции могут выполняться параллельно.

В этом отношении он параллелен прямо из коробки. Не нужно беспокоиться о параллельном запуске тестов в качестве оптимизации.

Есть по-настоящему хорошая статья, в которой объясняется, как это работает, и затрагиваются основы BEAM, OTP, планировщика, балансировщика нагрузки и т. Д.

Но ждать! Невозможно написать какое-либо разумное программное обеспечение без сохранения состояния!

Это совершенно правильно. Иногда двум процессам может потребоваться доступ к одному и тому же фрагменту данных. Например, задача обработки фонового задания. Существует очередь заданий, в которой один процесс добавляет задания в очередь, а другие процессы выполняют и удаляют задания из очереди.

Примеси

При работе с реальным миром код должен взаимодействовать с данными в разных состояниях. Это означает, что в конечном итоге возникнет потребность в побочных эффектах, создающих нечистые функции. Например:

  • Управление данными из базы данных (на диске или в памяти)
  • Управление локальными файлами
  • Работа с STDIN / STDOUT

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

Оставаясь в моем счастливом пузыре

В Elixir есть способ сделать это, используя модель актера или GenServer - они вроде как используют тот же принцип за кулисами. Оба являются реализацией паттерна актера для отправки и получения сообщений.

Основная идея состоит в том, чтобы поддерживать в рабочем состоянии один процесс, единственная цель которого - управлять состоянием, за которое он отвечает, и отвечать / получать сообщения от других процессов.

Каждый отдельный процесс, который отправляет сообщение процессу управления состоянием, немедленно получает ответ [ok|error] и может продолжить работу, не дожидаясь ожидания.

А как насчет условий гонки?

Они будут обрабатываться процессом сохранения состояния, а не вызывающим процессом. В примере с балансом «банковского счета» ошибочные записи (снятие большего количества, чем есть на счете) будут обнаружены процессом сохранения состояния, который, в свою очередь, вернет error каждому процессу, который его вызывает.

Таким образом, процессам, зависящим от штата, не нужно беспокоиться о записи недействительных данных. Если вы попытаетесь это сделать, процесс сохранения состояния остановит вас.

Мне нужен контроль

Существует Модуль поведения супервизора для управления иерархией процессов или дерево супервизора.

Потому что, ну, у вас не может быть группа детей, и они могут свободно бегать. Что, если один из них умрет, застрянет или заблудится? Что, если один из них плохо себя ведет и его нужно зверски убить?

Я думаю, что дерево наблюдения также будет очень полезно для устойчивости системы. Я не совсем уверен, как это сработает, но если супервизор сможет перезапустить мертвого ребенка автоматически, это поможет всей системе восстановиться с минимальным влиянием на другие ее части.

Путешествие так далеко

Из моего опыта обучения можно сделать несколько важных выводов. Они сосредоточены на вещах, которые я могу вернуть в мир Ruby или ООП, что сделало бы мои системы лучше.

1. Защитите чистые функции

Они очень быстрые и надежные. Но чаще всего они приправлены побочными эффектами при чтении чего-либо из БД или чего-то подобного. Эти примеси приводят к их соединению с вводом-выводом, который является медленным, ненадежным и трудным для тестирования.

Один из способов сделать это - использовать монады для помощи с потоком управления в Ruby. Предостережение здесь заключается в том, что все данные, поступающие от ввода-вывода, должны быть загружены перед их обработкой, а все данные, которые необходимо записать обратно в ввод-вывод, должны быть отправлены после. Сама монада должна быть чистой.

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

2. Рекурсия на расстоянии вытянутой руки

Использование рекурсии вместо обычных циклов в императивных языках, вероятно, действительно не требуется в повседневной жизни. Однако полезно знать.

Видя, что большинство программистов на императивных языках не используют рекурсию, использование рекурсии, когда в ней нет необходимости, может даже оказаться медвежьей услугой, поскольку другим членам вашей команды будет излишне трудно это понять.

При этом хорошо понимать, как это работает, и знать, как написать простую рекурсивную функцию на всякий случай.

3. Продолжайте учиться

Ruby - отличный язык, но у него есть неизбежные недостатки. Всегда найдутся лучшие инструменты для разных работ.

Изучение других языков / парадигм помогает обогатить мои знания как разработчика, чтобы я мог лучше решать проблемы в будущем.

Ссылки и ресурсы

Ниже приведены несколько ссылок на ключевые ресурсы, которые помогли мне изучить Elixir за последние несколько месяцев.