Это беззаботное изложение моего опыта изучения 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 за последние несколько месяцев.