DEV Community

собачья будка
собачья будка

Posted on • Edited on

прагматичное функциональное программирование [перевод]

Вольный перевод статьи Robert C. Martin "Pragmatic Functional Programming"

Прагматичное функциональное программирование

Всерьез движение к функциональному программированию началось лет десять назад. Языки, вроде Scala, Clojure и F#, начали привлекать внимание. Энтузиазм был чем-то большим, чем обычное “О, круто, новый язык!”. И было в нем что-то особенное, или мы так думали.

Закон Мура говорил нам о том, что вычислительные мощности компьютеров будут удваиваться каждые 18 месяцев, и вплоть до 2000-х годов он работал, а после перестал. Полностью. Тактовая частота дошла до 3Гц и там остановилась, скорость света была достигнута, сигналы перестали проходить сквозь поверхность чипа настолько быстро, чтобы реализовать более высокие скорости.

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

Первая двухъядерная машина появилась у меня 8 лет назад. Два года назад я перешел на четырехъядерную. Тогда и началось повальное увеличение количества ядер отовсюду. Там мы и поняли, что на разработке ПО это отразится так, как мы и представить себе не могли.

Одним из наших ответов было изучение функционального программирования, которое запрещает изменять состояние переменной после её инициализации, что отлично сказывается на многопоточности. Если состояние переменной не может меняться, то не возникнет конкуренции (race condition). Если значение переменной не может обновляться, то не возникнет проблемы параллельного обновления (concurrent update problem).

Это, конечно, подразумевалось как решение проблемы многоядерного процессора. Пока количество ядер росло, параллельность, НЕТ! — одновременность, могла стать существенной проблемой. ФП было вынуждено предложить стиль программирования, который позволял бы облегчить работу с 1024 ядрами в одном процессоре.

Так все начали учить Clojure, или Scala, или F#, или Haskell, поскольку знали, товарный поезд уже движется в их сторону, и к его приезду нужно быть готовыми.

Но поезд не пришёл. Шесть лет спустя я заимел четырехъядерный ноутбук, а после него еще два, да и последующий, похоже, тоже будет четырехъядерным. Еще одно затишье?

Прошлым вечером я смотрел фильм 2007 года, где героиня использовала ноутбук, серфила интернет в модном браузере, пользовалась гуглом и получала сообщения на свою раскладушку. Всё было таким знакомым. О, была и дата. Я видел, что ноутбук, браузер и раскладушка были далеки от современных. Но сегодняшние перемены не настолько глобальные, какие были в период с 2000 по 2011, и уж точно не настолько, как в период с 1990 по 2000. Выходит, что компьютеры и ПО находятся в застое?

Потому, возможно, ФП - не настолько критичный навык, как мы когда-то думали. Возможно, не всё будет завалено ядрами. Возможно, нам не стоит волноваться о чипах с 32,768 ядрами. Возможно, нам стоит расслабиться и вернуться к принципу изменяемости переменных.

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

Почему? Начнем с той причины, которая интересовала нас с самого начала. Благодаря ФП, синхронность становится намного более безопасной. Если вы строите систему со множеством связей или процессов, то использование ФП значительно сокращает количество ошибок, которые могут появиться при состоянии гонки или параллельного обновления.

Почему еще? Ну, ФП проще писать, проще читать, проще тестировать и проще понимать. Могу представить, как сейчас некоторые из вас болтают руками и кричат в монитор. Вы попробовали ФП и не заметили, в каком это месте оно простое. Все эти мэпы, редьюсы и рекурсии - особенно хвостовая - ни разу не просты. Конечно, я понимаю. Но проблема только в том, что с ними вы не знакомы. Но, как только вы освоите эти концепты, программировать вам станет гораздо проще.

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

Какой же язык ФП вам стоит использовать? Мой любимчик - Clojure, потому что он прост до абсурда. Clojure - диалект Lisp`а, который сам по себе невероятно прост. Я покажу.

Допустим, у нас есть следующая Java функция: f(x);

И, чтобы превратить ее в функцию в Lisp, нужно просто сдвинуть первую скобку влево: (f x).

Теперь вы знаете 95% языка Lisp и 90% языка Clojure. Этот простой скобочный синтаксис и есть вся весь синтаксис этих языков. Они абсурдно просты.

Может, вы уже видели программы на Lisp раньше, и вам не понравились все эти скобочки, или все эти CAR , CDR ,CADR и прочее, но не беспокойтесь. В Clojure немного больше пунктуации, чем в Lisp, поэтому скобок там меньше, а еще в Clojure заменены CAR , CDR и CADR на firstrest и second. Более того, Clojure построен на JVM и предоставляет вам полный доступ ко всей Java библиотеке, или любому Java фреймворку, или любой другой библиотеке. Совместимость быстрая и легкая. И, что еще лучше, Clojure даёт полный доступ к объектно-ориентированным фичам JVM.

“Но постойте, ведь ФП и ОО несовместимы друг с другом!”. Кто вам такое сказал? Это нонсенс! В ФП вы не можете менять состояние объекта, ну и что с того? Если запушить целое число в стэк – получите новый стэк. С тем же успехом, когда вы вызовете метод, устанавливающий значение объекта - получите новый объект. С этим очень просто справиться, как только вы к этому привыкнете.

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

(defprotocol Gateway
    (get-internal-episodes [this])
    (get-public-episodes [this]))
Enter fullscreen mode Exit fullscreen mode

Код выше определяет полиморфный interface для JVM. В Java этот interface будет выглядеть таким образом:

public interface Gateway {
    List<Episode> getInternalEpisodes();
    List<Episode> getPublicEpisodes();
}
Enter fullscreen mode Exit fullscreen mode

На уровне JVM создаваемый байт-код идентичен. И ведь правда, что программа, написанная на Java, реализует интерфейс так же, как Java. Программа Clojure точно также может реализовать Java интерфейс. Это будет выглядеть так:

(deftype Gateway-imp [db]
    Gateway
    (get-internal-episodes [this]
        (internal-episodes db))

    (get-public-episodes [this]
        (public-episodes db)))
Enter fullscreen mode Exit fullscreen mode

Обратите внимание на аргумент конструктора db, и как все методы могут иметь к нему доступ. В данном случае реализации интерфейса просто передаются локальным функциям, посредством передачи db.

А самое лучшее, так это то, что Lisp, а значит и Clojure (погодите) Гомоиконны. Что это значит? То, что код – это данные, которыми программа может управлять. Это легко заметить. Этот код: (1 2 3) представляет собой список из трёх целых чисел. Если первый элемент списка — функция, как тут: (f 2 3), то список становится вызовом функции. Потому все функции в Clojure — это списки, а списками можно управлять напрямую. Значит, программа может собирать и исполнять другие программы.

Подведу итоги. Функциональное программирование - очень важная вещь, и вам стоит его изучить. А если вы не уверены насчет языка - я советую Clojure.

Top comments (0)