В последние несколько дней на Хабре был опубликован ряд статей, общим лейтмотивом которых (особенно в комментариях) стало противостояние тупоконечников с остроконечниками – адепты ФП против ООП, хотя их и призывали не спорить. Иногда обсуждали Erlang, в связи с чем мне вспомнился короткий пост на тему от Джо Армстронга, одного из создателей этого языка, написанный им в конце 2018 года на форуме по Elixir в ответ на вопрос о парадигме языка. Думаю, его комментарий будет интересен.
Джо Армстронг, 12 сентября 2018 года
Всё хорошее в Elixir (и Erlang) связано с параллелизмом – простое создание независимых процессов и обмен сообщениями. И именно это является самой сущностью объектно-ориентированного программирования, на что неоднократно указывал Алан Кей.
ООП полностью посвящено объектам. Объекты отвечают (или должны отвечать) на сообщения, и когда вы хотите что-то сделать – вы посылаете объекту сообщение, а уж как он обработает его — совершенно не важно. Думайте об объектах как о "черных ящиках", и когда вы что-то хотите от них — просто посылайте сообщения, а они вам будут слать сообщения в ответ.
Как всё устроено внутри не имеет значения – будет ли код внутри "черного ящика" функциональным или императивным – важно лишь то, что именно он должен делать.
К сожалению, хоть первый успешный объектно-ориентированный язык на основе этой модели (Smalltalk) и оперировал понятиями "объект" и "сообщение", но последние в Smalltalk были не настоящими сообщениями, а лишь замаскированными синхронными вызовами функций. Эта же ошибка была повторена в С++, потом в Java, и главная идея ООП выродилась в странную парадигму организации кода по классам и методам.
Erlang и Elixir обеспечивают легкое создание миллионов изолированных процессов, где всё работает через посылку сообщений между ними. Архитектура системы определяется тем уровнем параллелизма, который вы желаете, с последующим отображением его на процессы непосредственно.
Web-сервер на Elixir для 10 000 пользователей это не "один web-сервер с 10 000 пользователей" (как в случае с Apache или Jigsaw и им подобным), но это "10 000 web-серверов на каждого пользователя" — согласитесь, радикальный отход от традиционной модели.
Тот факт, что для описания модели процессов Erlang/Elixir был использован простой функциональный язык – почти случайность. Всё началось с системы логического программирования (Prolog), а такие вещи, как, например, C-node, могут быть написаны вообще на любом языке. По-настоящему важным для Elixir (и любого другого BEAM-языка) является способность их виртуальной машины оперировать экстремально большим количеством параллельных процессов.
Долгое время я говорил, что "Erlang это единственный настоящий объектно-ориентированный язык". Наверное, теперь я могу добавить к нему и Elixir.
Для ООП базовыми вещами являются:
- изоляция между объектами (у нас это есть)
- позднее связывание (мы решаем что делать только тогда, когда процесс получит сообщение)
- полиморфизм (все объекты могут отвечать на сообщение одного типа, например, "print-yourself" и любой объект может знать, как это выполнить)
Гораздо менее важно:
- разделение на классы и методы
- синтаксис
- программная модель (функциональная или императивная)
После того, как мы разделили систему на большое количество небольших, общающихся между собой процессов, всё остальное становится (относительно) легко – каждый процесс должен оказаться достаточно простым и уметь делать довольно мало, что сильно упрощает программирование.
То, что Erlang (и Elixir) привнес в программирование, было идеей связей (link – прим. переведчика). Изначально предложенная Майком Вильямсом, она заключается в расширении возможности обработки ошибок, позволяя её делать за границами процессов. Имея это мы получили все необходимые инструменты для построения деревьев супервизоров и т.п.
Супервизоры, gen_server'ы и всё такое прочее – это всего-навсего библиотеки, которые скрывают от пользователя некоторые подробности. Простые внутри, написанные с помощью тех же инструментов – параллельных процессов и связей между ними.
Erlang был разработан не как язык функционального программирования, а как инструмент создания долгоживущих отказоустойчивых систем.
Центральным элементом отказоустойчивости является понятие удаленной обработки ошибок. Если вся система выходит из строя, неисправность должна быть исправлена (компенсирована) на другой машине, так как локально это сделать уже невозможно — локальный компьютер не работает.
Это означает, что для программирования отказоустойчивых систем нам нужно, чтобы распределение (процессов) и обмен сообщениями были простыми в использовании инструментами, и именно поэтому, в принципе любая отказоустойчивая архитектура в конце концов окажется похожей на Erlang.
Весь смысл создания Erlang состоял в том, чтобы упростить программирование отказоустойчивых систем, побочным эффектом чего явилась легкость программирования масштабируемых систем.
Различие между Erlang и Elixir и «всеми остальными» заключается в механизмах обеспечения параллелизма и отказоустойчивости, и речь тут не о монадах, синтаксисе, или "чистоте" ФП.
Теперь вопрос – хотите ли вы обработать 10 000 пользователей в одном потоке, используя коллбеки для эмуляции параллелизма, или вы все же хотите создать 10 000 параллельных процессов, каждый из которых прост и не нуждается в коллбеках вовсе?
Каждый процесс ожидает сообщение, в котором он заинтересован, затем выполняет вычисления и засыпает в ожидании следующего.
Я думаю, что большая проблема популяризации Erlang/Elixir состоит в том, что вам нужно объяснить, как большое количество параллельных процессов помогает решить вашу конкретную проблему. Поскольку никакие другие распространенные языки изначально не нацелены на параллельное программирование и не облегчают его сколь-нибудь значимым образом, необходимость в нём для людей не до конца осознана и понятна.
"Но я могу делать всё на коллбеках в одном потоке" – скажут они. И они это делают, и это мучительно сложно. И вы спрашиваете "что произойдет, если коллбек попадет в цикл или вызовет исключение?", и если они не понимают вопроса – то тут вы должны потрудиться и объяснить проблему. Но если вопрос окажется понятен – то расскажите им, что есть одна далёкая страна, в которой для программирования параллелизма коллбеки не нужны.
Кажется, всё вышесказанное можно сократить до следующего: пожалуйста, не рекламируйте Elixir как язык функционального программирования – он им не является. Он язык параллельного программирования (CPL, Concurrent Programming Language).
Не реагируйте на аргументы вида "мой функциональный язык функциональнее твоего". И даже не вздумайте говорить о монадах, сразу меняйте тему.
" — Что такое CPL?"
" — Ты знаешь, это то, на чем сделан WhatsApp ..."
От переводчика
Прежде всего, хотелось бы выразить сожаление в связи со скоропостижной смертью Джо Армстронга — он скончался 20 апреля 2019 года. Недооценить его вклад в развитие индустрии сложно, равно как и его талант популяризатора — книги, выступления, активная деятельность в сообществах Erlang и Elixir.
Полезные ссылки:
- Джо Армстронг
- Алан Кей
- Майк Вильямс
- Learn You Some Erlang for Great Good!
- Краткое введение в Erlang C-node
Автор: Дмитрий Стропалов