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

Привет, Хабр! Предлагаю вашему вниманию перевод статьи «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.

Комментарии 20

    +1
    Оффтопик
    Гуглопереводчик наконец-то довели до ума. Ну, почти. :)

      0
      Ответ на оффтопик
      Нет, ребята сами редактировали, после просьбы модератора сделать перевод «человекочитаемым» и осознать, что в нём не так. Стало значительно лучше, чем было. Обучаем понемногу, не только за порядком следим ;-)
        0
        Оффтопик
        Тем не менее, «машинность» (или, если хотите, «английскость») текста бросается в глаза. Оно и не удивительно, учитывая большую разницу между английским и русским языками. Хотя, возможно, у меня просто глаз наметанный.

        P.S. Извиняюсь за брюзжание. В последнее время стараюсь воздерживаться от подобных комментариев (понимаю, что толку от них мало), но иногда не удается сдержать раздражение…
          +1
          Ещё ответ
          Бросается, бросается. На самом деле, когда вычитываю, даже в голове читается «робоголосом». Но поскольку ребятам надо и срочно, поработали и остановились на MVP-переводе :-)
            –1
            Но поскольку ребятам надо и срочно, поработали и остановились на MVP-переводе :-)

            Поскольку надо и срочно? И что ещё можно ребятам поскольку надо и срочно? И это как, только ребятам можно или может всем? Можно мне карму поднять до тысячи, а то мне надо и срочно? Или можно я вышлю список имейлов, им инвайты нужны. Им надо и срочно, как будут инвайты?

              +1
              Ваше возмущение совершенно не обосновано, вы придираетесь к словам. Вообще я удивлена, что читаю его именно от вас.

              • Мы выпускаем статьи только те, которые и так были бы выпущены и соответствуют правилам.
              • Мы отрабатываем перевод с каждым из них, возвращая статьи обратно — иногда по 5 раз. Сейчас у них 3 совершенно отклонённые статьи, ещё несколько — на доработке по всей строгости.
              • В переводах минимум ошибок и проблем. Про MVP — это, разумеется, шутка.
              • Единственное допущение — срочность, мы их выпускаем чуть быстрее. Но это доступно всем — если статья в Песочнице реально нормальная, а не мусор.

              Ни один перевод со старого источника, с рекламой, не по теме ИТ, с плохим качеством — не прошёл и не пройдёт.
                –2
                Ваше возмущение совершенно не обосновано, вы придираетесь к словам.

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


                Вообще я удивлена, что читаю его именно от вас.

                Что особенного в моей персоне?


                Мы отрабатываем перевод с каждым из них, возвращая статьи обратно — иногда по 5 раз.

                А кто эти загадочные "они". Если бы это была какая-то компания, это бы всё объяснило, но статья не опубликована в корпоративном блоге. Особенно ужасно это выглядит в свете того, что в песочнице тексты анонимизируются, для того, чтобы их оценивали по качеству а не по принадлежности автора к какой-то группе, которой очень надо.


                И как себя после этого будет чувствовать человек, который пишет в песочницу, но инвайт не получил?


                В переводах минимум ошибок и проблем.

                Ну вообще я не о качестве перевода сейчас, не оно меня так взволновало. Но тем не менее я близок к тому, чтобы через часик опубликовать свой перевод. Потому что я не очень хорошо понимаю, как можно было фразу.


                The next laptop I get looks like it will be a four core laptop too.

                Перевести как:


                Следующий ноутбук, который я получил, внешне выглядел так, как будто он тоже четырехъядерный.

                Такое ощущение, что текст и правда не переводили, а правили гугл транслейт.


                Единственное допущение — срочность, мы их выпускаем чуть быстрее.

                Теперь я вообще не понимаю что происходит. Какая срочность? Почему я когда публиковал статьи в песочнице не увидел кнопку — мне срочно ))? Серьёзно, что происходит? Если у хабра договор с какой-то компанией, почему перевод не в блоге компании? Если нет, почему нарушены правила получения инвайтов из песочницы? В чём дело то?

                  0
                  Я вам отвечу в л.с., извините, не хочу разводить флуд. Не нужно здесь на ровном месте устраивать странные разборки, честно. Если у вас есть вопросы по переводу, вы можете направить их автору.
              0
              А в чём такая необходимость и срочность в выкладывании статьи? Последние переводы Боба на сайте таковы, что он выглядит каким-то старым клоуном. И самое главное, теряется смысл интересных статей.
                0
                Только что узнала, что Uncle Bob едет в Россию :-) habr.com/ru/company/jugru/blog/478308
                  0
                  Роберт Мартин — не Металлика, его вполне можно и в записи на ютубе посмотреть :)
        +1

        Кто смог понять словосочетание "динамическая отправка", не подглядывая в первоисточник?

          0
          Осилил угадать. К чему не привыкнешь читая некоторые переводы…
            0
            По мотивам дикой карты/wildcard
            динамическая диспетчеризация?
            0

            Clojure очень приятный язык с большой встроенной стандартной библиотекой, и безусловно как lisp дает новые ощущения от программирования ( хотя в работе с состоянием не дотягивает до Common lisp). Но я бы не стал советовать его прагматичным разработчикам, к сожалению состояние библиотек оставляет желать лучшего (многие из них не продакшен ready качества) и в целом активность community гораздо ниже чем пару лет назад. Стоит взглянуть в сторону F#, Ocaml и Haskell (на его прагматичную часть)

              0
              К примеру на ФП легче писать, легче читать, легче тестировать и легче понимать

              Теоретически. До тех пор, пока не захочется поставить брейкпоинт и тупо задебажить. И вот когда нужно будет дернуть окружение, базу там, файл, сокеты, причем в определенной последовательности — тут начнутся сложности… А вовсе не в хвостовой рекурсии.


              Потому, что вам не нужно отслеживать состояние системы. Состояние переменных может не измениться; таким образом, состояние системы остается неизменным.

              Не совсем верно. Данные остаются неизменными, зато меняется композиция над этими данными. Что проще: остановить Вселенную и пошагово наблюдать эволюцию состояний во времени, либо докапываться какую функциональную композицию в неявном виде передали ввиде аргумента в ту или иную функцию?

                0
                >> Потому, что вам не нужно отслеживать состояние системы.
                но ведь от наших вычислений должен же быть какой-то эффект? Мы взялись что-то вычислять, вычислили и результат должны отправить за пределы компьютера. Для этого в качестве промежуточного шага надо записать результат в оперативную память и тем самым изменить состояние системы. Процесс вывода результата из компьютера должен отслеживать это состояние и начать вывод, как только результат получен. Наверняка в функциональном программировании эта задача как-то решается, и решаются возникающие проблемы с синхронизацией, когда результат объемный и выводится по частям. Но почему-то сплошь и рядом функциональщики пытаются меня убедить, что у них состояние системы не меняется и никаких проблем с синхронизацией процессов у них нет. Не понимаю, почему нельзя честно рассказывать о своих проблемах и путях их решения, как это делают все остальные программисты?
                  0
                  Исходное состояние в идеале и не должно меняться. Результат вычислений — новые данные; новое состояние, если хотите.
                    0
                    >>> Исходное состояние в идеале и не должно меняться.
                    Исходное — да. Но нам-то втирают о состоянии всей системы, которое состоит из исходного, конечного и, возможно, промежуточных состояний. Результат вычислений — да, это новые данные, и их появление означает изменение состояния системы.
                • НЛО прилетело и опубликовало эту надпись здесь

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое