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

в 9:55, , рубрики: clojure, functional programming, uncle bob, Совершенный код, функциональное программирование

Движение к функциональному программированию началось всерьез примерно десятилетие назад. Мы видели как такие языки как Scala, Clojure и F# стали привлекать внимание. Это движение было больше чем просто обычное восхищение «О, круто, новый язык!». Было что-то действительно побуждающее это движение — или мы так думали.

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

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

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

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

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

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

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

Кстати, вчера вечером я смотрел фильм 2007 года. Героиня использовала ноутбук, просматривала страницы в модном браузере, использовала google, и получала текстовые сообщения на телефон-раскладушку. О, оно было датировано — я мог видеть что ноутбук был более старой модели, браузер был более старой версии и телефон был далек от современных смартфонов. И всё же это были не такие впечатляющие изменения, какими были изменения между 2000 и 2011. И даже близко не такие впечатляющие, как изменения между 1990 — 2000. Наблюдаем ли мы стабилизацию в темпах компьютерных и программных технологий?

Что ж, возможно, ФП не такой критический навык как мы однажды думали. Может быть мы не будем погребены под ядрами. Может быть нам не стоит беспокоиться о чипах с 32768 ядрами. Может быть мы все можем расслабиться и снова вернуться к обновлению наших переменных.

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

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

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

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

Так какой язык ФП вам следует использовать? Мой любимый — Clojure. Причина того что это Clojure до абсурда проста — это диалект Lisp, прекрасно простого языка. Позвольте продемонстрировать.

Это функция в Java: f(x);
Теперь, чтобы превратить ее в функцию на Lisp, просто переносим первую скобку влево: (f x).

Теперь вы знаете 95% Lisp, и знаете 99% Clojure. Немного дурацкого синтаксиса со скобками — это на самом деле почти всё что касается синтаксиса в этих языках. Они абсурдно просты.

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

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

Но вернемся к ООП. Одна из особенностей ООП которую я нахожу наиболее полезной, на уровне программной архитектуры, это динамический полиморфизм. И 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, являются (подождите, подождите) гомоиконичными, что значит что код это данные которыми программа может манипулировать. Это легко увидеть. Следующий код (1 2 3) представляет список из трех целых чисел. Если первый элемент оказался функцией, как в (f 2 3), тогда это становится вызовом функции. Таким образом, все вызовы функций в Clojure являются списками, а списками можно манипулировать из кода. Таким образом, программа может создавать и выполнять другие программы.

Суть вот в чём. Функциональное программирование важно. Вам следует изучать его. И если вы беспокоитесь о том, с помощью какого языка его изучать, я рекомендую Clojure.

Автор: Adamovskiy

Источник

Поделиться

* - обязательные к заполнению поля