2 December 2019

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

JavaLispFunctional ProgrammingClojure
Sandbox
Привет, Хабр! Предлагаю вашему вниманию перевод статьи «Pragmatic Functional Programming» автора Robert C. Martin (Uncle Bob).

Переход к функциональному программированию всерьез развился только около десяти лет назад. Мы видим, что такие языки, как Scala, Clojure и F# привлекают внимание. На самом деле это был большой шаг в программировании: “О, круто, новый язык!” — энтузиазм… Видимо там было что-то особенное — ну или это мы так думали.  

Закон Мура гласит нам, что скорость компьютеров будет удваиваться каждые 18 месяцев. Данный закон действовал с 1960-х до 2000 годов. Затем он прекратился. Частота достигла 3 ГГц, а затем и вовсе поднялась на плато. Мы достигли скорости света! Сигналы не могут распространяться по поверхности чипа достаточно быстро, чтобы обеспечить более высокие скорости. 

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

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

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

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

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

Но товарный поезд так и не пришел. Как я уже говорил раннее, шесть лет назад у меня появился четырехъядерный ноутбук. С того момента у меня было еще 2 таких же ноутбука. Следующий ноутбук, который я получил, внешне выглядел так, как будто он тоже четырехъядерный. Видимо мы видим еще одно плато (закона Мура)?
Кроме того, вчера вечером я смотрел фильм 2007 года. В этом фильме героиня пользовалась ноутбуком, просматривала страницы в браузере, пользовалась Google и получала смски на свой телефон. Мне это было слишком знакомо. Ох… это было олдскульно — в этом фильме я увидел довольно-таки старый ноутбук с древней версией браузера, а раскладной телефон сильно отличался от современных смартфонов. Тем не менее изменение не было таким существенным, как изменение с 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 на first, rest и second. Clojure построен на JVM и позволяет получить полный доступ ко всей библиотеке Java и любой другой инфраструктуре Java или библиотеке, которую вы используете. Функциональная совместимость является быстрой и простой. И что еще лучше, Clojure обеспечивает полный доступ к функциям OO в JVM.

«Но подождите!» Я слышу, как вы говорите. «Как же так, ФП и OO взаимно несовместимы!» Кто вам это сказал? Это полная чепуха. Но то что в ФП вы не можете изменить состояние объекта это правда; ну и что? Подобно тому, как вставка целого числа в стек дает вам новый стек, когда вы вызываете метод, который корректирует значение объекта, вы получаете новый объект вместо того, чтобы изменить старый. С этим очень легко справиться, как только вы к этому привыкнете.

Но давайте всё-таки вернемся к ОО. Одной из особенностей ОО, которые я считаю наиболее полезными на уровне архитектуры программного обеспечения, является динамический полиморфизм. А Clojure предоставляет полный доступ к динамическому полиморфизму Java. Данный пример поможет мне это объяснить лучше всего.

(defprotocol Gateway
  (get-internal-episodes [this])
  (get-public-episodes [this]))

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

public interface Gateway {
	List<Episode> getInternalEpisodes();
	List<Episode> getPublicEpisodes();
}

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

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

  (get-public-episodes [this]
    (public-episodes db)))

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

Возможно лучше всего то, что Lisp и Clojure, являются Homoiconic, что означает, что код — это данные, которыми программа может манипулировать. Это легко увидеть. Следующий код: (1 2 3) представляет список из трех целых чисел. Если первый элемент списка оказывается функцией, как в: (f 2 3), то он становится вызовом функции. Таким образом, все вызовы функций в Clojure являются списками; и списки могут напрямую управляться кодом. Таким образом, программа может создавать и выполнять другие программы.

Суть заключается в следующем: функциональное программирование важно и вы должны изучить его. И если вам интересно, какой язык лучше использовать чтобы выучить его, то я предлагаю вам Clojure.
Tags:перевод с английскогоjavaclojurelispкод
Hubs: Java Lisp Functional Programming Clojure
+7
5.3k 46
Comments 20