Это перевод статьи “Pragmatic Functional Programming” Роберта Мартина (дядюшки Боба).
Подвижка в сторону функционального программирования произошла, признаться честно, около десяти лет назад. Мы заметили, что языки вроде Scala, Clojure, and F# начали привлекать внимание. Это было больше, чем привычный энтузиазм — “О, круто, новый язык!”. Было что-то выделявшее его, по крайней мере мы так думали.
Закон Мура обещал нам, что скорость компьютеров будет удваиваться каждые 18 месяцев. Этот закон работал с 1960 до 2000. А затем остановился. Вообще. Частота тактовых импульсов достигла 3Гц и перестала подниматься. Скорость света была достигнута. Сигналы не могли проходить сквозь поверхность чипа настолько быстро, чтобы реализовать более высокие скорости.
Дизайнеры железа изменили стратегию. Чтобы получить большую производительность, они добавили больше процессоров (ядер). Чтобы освободить место для этих ядер, они убрали большую часть кэша и конвейерную архитектуру из чипов. Несмотря на то, что процессоры стали немного медленнее, чем раньше, их стало больше. Производительность увеличилась.
Первый двухядерный компьютер у меня появился 8 лет назад. Два года спустя я приобрёл четырёхядерный. С того момента началось резкое увеличение количества ядер процессора. И мы все поняли, что это отразится на разработке софта так, как мы себе не могли представить.
Одной из наших реакций было изучать функциональное программирование (ФП). ФП настоятельно рекомендует не изменять состояние переменной после её инициализации. Это отлично сказывается на многопоточности. Если вы не можете изменить состояние переменной, race condition исключается. Если вы не можете обновить значение переменной, исключается задача многопоточного обновления.
Это, конечно, подразумевалось как решение проблемы многоядерного процессора. Так как количество ядер продолжало расти, многопоточность, даже нет — одновременность, могла стать значимой проблемой. ФП было вынуждено предложить стиль программирования, который позволял бы мигрировать задачи, чтобы справляться с 1024 ядрами в одном процессоре.
Тогда все начали изучать Clojure, Scala, F# или Haskell, потому что знали, что в их направлении движется грузовой состав и хотели быть готовыми к моменту его прибытия.
Но состав так и не прибыл. Шесть лет назад я приобрёл четырёхядерный ноутбук. После этого у меня было ещё два. Следующий лаптоп скорее всего тоже будет четырёхядерным. Что, опять застой?
Маленькое отступление. Прошлым вечером я смотрел фильм 2007 года. Героиня использовала ноутбук, просматривая страницы в популярном браузере, искала что-то через гугл и на телефон-раскладушку ей приходили смс. Всё было очень похожим. И старым. Было заметно, что ноутбук не новой модели, браузер устаревшей версии, а телефон-раскладушка — печальный отголосок сегодняшних смартфонов. Но сегодня всё изменилось не настолько масштабно, как с 2000 по 2011. И изменения сильно далеки от тех, что произошли с 1990 по 2000. Неужели мы вошли в стагнацию компьютерных технологий и софта?
Возможно, ФП не настолько критичный навык, как мы когда-то предполагали. Возможно мы не потонем в море ядер. Может быть нам не нужно беспокоиться о 32768-ядерных чипах. Может быть, мы можем расслабиться и вернуться к обновлению своих переменных.
Но мне кажется это будет ошибкой. Колоссальной. Мне кажется это будет такой же серьёзной ошибкой, как безудержное использование goto. И настолько же опасно, как отказ от динамической диспетчеризации.
Почему? Можно начать с причины, которая беспокоила нас с самого начала. ФП делает синхронность намного безопасней. Если вы строите систему со множеством связей или процессов, то использование ФП значительно сокращает количество ошибок, которые могут появиться при состоянии гонки или многопоточных обновлениях.
Ещё? ФП проще писать, читать, тестировать и понимать. Я представляю, как некоторые из вас сейчас машут руками и кричат на экран. Вы пробовали ФП и оно вам показалось далеко не лёгким. Все эти преобразования, редукции и рекурсии, особенно хвостовая рекурсия — ну никак не легкая штука. Конечно. Я всё это понимаю. Но всё это — проблема привычки. Как только вы привыкните к этим концептам (а на выработку привычки не уйдёт много времени), программирование станет намного проще.
Почему оно станет проще? Потому что вам не нужно будет следить за состоянием системы. Состояние переменных не будет изменяться, поэтому состояние системы будет неизменным. И исчезнет необходимость в отслеживании не только состояния системы. Вам не нужно будет отслеживать состояние списка, набора, стека или очереди, потому что эти структуры данных не могут измениться. Когда вы пушите элемент в стек в языке ФП, вы получаете новый стек, а не изменяете старый. Это значит, что нужно будет жонглировать с меньшим количеством шариков в воздухе. Меньше запоминать. Меньше отслеживать. А код при этом намного проще писать, читать, понимать и тестировать.
Так какой же язык ФП стоит использовать? Мой любимый — Clojure. Причина в том, что Clojure простой до абсурда. Это диалект Lisp, а Lisp красивый и простой язык. Давайте я вам покажу.
Вот функция в Java: f(x);
Теперь превратим её в функцию в Lisp. Просто сдвигаете первую круглую скобку влево: (f x).
Когда вы знаете 95% Lisp, вы знаете 90% Clojure. Этот простой скобочный синтаксис и есть вся фишка синтаксиса в данных языках. Они абсурдно просты.
Возможно, вы видели программы на Lisp, и вам не понравились все эти скобки. Может быть, вам не нравятся всякие CAR, CDR и CADR. Не беспокойтесь. В Clojure немного больше пунктуации, чем в Lisp, поэтому скобок там меньше. В Clojure CAR, CDR и CADR заменены на first, rest и second. В дополнение Clojure построен на JVM и даёт полный доступ ко всей библиотеке Java, и любой другой библиотеке или фреймворку Java, которые вы хотите использовать. Совместимость быстрая и лёгкая. Что ещё лучше, Clojure даёт полный доступ к объектно-ориентированной (ОО) функциональности JVM.
Я слышу, как вы говорите: “Погоди!”. “ФП и OO взаимонесовместимы!” Кто вам такое сказал? Это бред! Правда только, что в ФП вы не можете изменить состояние объекта, ну и что? Если запушить целое число в стек — это даст вам новый стек. Точно также когда вы вызовете метод, который устанавливает значение объекта, вы получите новый объект вместо изменения старого. С этим очень просто справиться, как только вы к этому привыкнете.
Но вернёмся к ОО. Одна из функциональностей ОО, которую я нахожу самой полезной на уровне архитектуры приложений, это динамический полиморфизм. А Clojure даёт полный доступ к динамическому полиморфизму Java. Возможно примером объяснить лучше всего.
Выше написанный код определяет полиморфический interface для JVM. В Java этот интерфейс выглядел бы так:
На уровне JVM байт-код на выходе идентичен. Программа на Clojure может реализовать Java-интерфейс. В Clojure это выглядит так:
Обратите внимание на аргумент конструктора db, и как все методы могут иметь к нему доступ. В данном случае реализации интрерфейса просто делегируют что-то локальным функциям, передавая db.
Но самое лучшее, возможно, это то, что Lisp, а значит и Clojure (готовы?) гомоиконны, что означает: код — это данные, которыми может манипулировать программа. Это легко увидеть. Вот этот код: (1 2 3) представляет из себя список из трёх целых чисел. Если первый элемент списка — функция, как тут: (f 2 3), то код становится вызовом функции. Поэтому все функции, вызываемые в Clojure — это списки, а списками можно напрямую манипулировать с помощью кода. Значит, программа может собирать и исполнять другие программы.
В заключение. Функциональное программирование — важная штука. Вам стоит его изучить. И если вы размышляете над тем, какой язык использовать, чтобы изучать ФП, я советую Clojure.
Перевод: Наталия Басс
Originally published at ru.hexlet.io.