[Перевод] Сочиняя ПО: Почему стоит изучать ФП на JavaScript?
Эта статья — часть серии статей «Составляя ПО» про функциональное программирование и различные техники создания программ на JavaScript ES6+, начиная с азов. Предыдущая часть: Сочиняя ПО: Введение
Забудьте все, что вы знали о JavaScript, и постарайтесь воспринять эту статью так, будто вы начинающий программист. Чтобы помочь вам, мы рассмотрим JavaScipt начиная с самых основ, так, как будто вы никогда не видели JavaScript. Ну, а если вы начинающий, то вам повезло. Наконец-то попробуем изучить ES6 и функциональное программирование с нуля! К счастью, все новые концепты будут изучены по ходу дела -, но не рассчитывайте слишком уж сильно на это.
Если вы опытный разработчик, уже знакомый с JavaScript или с каким-то чисто функциональным языком, то вы можете подумать, что JavaScript это весьма забавный способ открыть для себя мир *[ФП]: функциональное программирование. Отставьте эти мысли в сторону и попробуйте посмотреть на текст незашоренным взглядом. Вы можете обнаружить скрытый уровень в программировании на JavaScript, уровень, о котором вы даже не подозревали.
Раз уж эта статья имеет в названии «Сочиняя ПО», и ФП это, очевидно, путь сочинить программу (используя функциональную композицию, функции высшего порядка, и т.д.), то вы можете спросить, почему бы нам не взять какой-нибудь Haskell, ClojureScript, или Elm вместо JavaScript.
JavaScript имеет в своем составе важные особенности, необходимые для ФП:
Функции первого класса. Это возможность использовать функции как данные, т.е. передавать функции в качестве входных параметров, возвращать функции и присваивать функции переменным и свойствам объектов. Это свойство делает возможным существование функций высшего порядка, что, в свою очередь, делает возможным появление частичного применения, каррирования и композиции.
Анонимные функции и лямбда-синтаксис. Например, запись вида
x => x * 2
является валидным выражением в JavaScript. Такой синтаксис значительно упрощает работу с функциями высшего порядка.Замыкания. Замыкание — это комбинация функции и ее лексического окружения. Замыкания создаются в момент создания функции. Когда функция создается внутри другой функции, то она имеет доступ к переменным, объявленным во внешней функции, даже после того, как будет осуществлен возврат из этой внешней функции. Замыкания это то, что позволяет работать фиксированным аргументам частичных применений. Фиксированный аргумент — это аргумент, заданный в контексте замыкания возвращаемой функции. Например, в выражении
add(1)(2)
аргумент1
является фиксированным аргументом для функции, возвращаемой при вызовеadd(1)
. Пример:
/*
* Более длинный вариант:
* const add = function (x) {
* return function (y) {
* return x + y;
* }
* }
*/
const add = x => y => x + y;
const summ = add(1)(2);
Что отсутствует в JavaScript
JavaScript — это универсальный язык, дающий возможность использовать несколько парадигм, т.е. позволяющий программировать в различных стилях. Эти другие стили включают в себя: процедурный (императивный) стиль программирования (как, например в Си), где функции представляют собой набор инструкций, который может быть вызван из другого места в программе; объектно-ориентированный стиль, где объекты -, а не функции — являются основным строительным блоком. Недостатком такого мульти-парадигменного подхода является то, что императивный и объектно-ориентированный стиль предполагают, что практически всё в коде должно быть изменяемым или подверженным мутации, мутабельным.
Мутация — это изменение значения структуры данных без создания новой переменной и переприсваивания значения. Например:
const foo = {
bar: 'baz'
};
foo.bar = 'qux'; // мутация
Обычно объекты должны быть изменчивы, чтобы значения их свойств могли быть изменены с помощью методов. В императивном программировании большинство структур данных являются мутабельными чтобы обеспечить эффективную работу с объектами и массивами.
Ниже представлен список вещей, которые присущи некоторым функциональным языкам и которых нет в JavaScript:
Чистота. В некоторых ФП языках «чистота» поддерживается самим языком. Выражения с побочными эффектами недопустимы.
Неизменчивость (иммутабельность). Некоторые ФП языки блокируют мутации. В замен мутаций существующих значений структур данных, например объектов и массивов, выражения помещают результат в новые структуры данных. Это может выглядеть неэффективно, но большинство ФП языков внутри используют специальные структуры данных (префиксное дерево, например) с возможностью совместного использования данных, т.е. старый объект и новый объект хранят ссылки на одни и те же данные, если они не менялись.
Рекурсия. Рекурсия — это способность функции вызвать саму себя. Во многих ФП языках рекурсия это единственная возможность выполнить цикл. В таких языках нет конструкций типа
for
,while
илиdo ... while
.
Чистота: В JavaScript «чистота» может быть достигнута только по соглашению (т.е. все участники должны договориться использовать только чистые функции — прим. перев.). Если вы не используете для большей части своего приложения композицию чистых функций, то вы не следуете функциональному стилю. К сожалению, в JavaScript слишком просто сбиться с пути случайно начав создавать и использовать не «чистые» функции.
Иммутабельность: В ФП языках иммутабельность зачастую дана по умолчанию. В JavaScript отсутствуют эффективные структуры данных, используемые в большинстве ФП языков, но существуют библиотеки, которые могут помочь в этом вопросе, например Immutable.js и Mori. Я надеюсь, что в будущих версиях спецификации ECMAScript все же появятся неизменяемые структуры данных.
Есть некоторые знаки, которые дают на это надежду, как, например добавление const
в ES6. Переменной, объявленной с помощью const
, не может быть присвоено другое значение. Важно понимать, что const
не делает значение переменной неизменчивым.
Если переменная const
указывает на объект, то значение переменной не может быть изменено полностью, однако могут изменены свойства объекта. В JavaScript имеется возможность заморозить объект с помощью freeze()
, но такие объекты замораживаются только на верхнем уровне, а это означает, что если замороженный объект имеет в каком-либо свойстве другой объект, то свойства этого другого объекта подвержены изменениям. Другими словами, JavaScript ещё предстоит долгая дорога к настоящей неизменности, закрепленной в спецификации языка.
Рекурсия: Технически, JavaScript поддерживает рекурсию, однако большинство функциональных языков имеют такую особенность как «оптимизация хвостовой рекурсии». Такая особенность позволяет рекурсивным функциям переиспользовать фреймы стека для последующих рекурсивных вызовов (фактически рекурсия преобразуется в плоскую итерацию — прим. перев.).
Без оптимизации хвостовой рекурсии стек вызовов растет без ограничений и может вызвать переполнение стека. JavaScript, с технической точки зрения, имеет ограниченную хвостовую оптимизацию в стандарте ES6. К сожалению, только один из наиболее распространенных браузеров реализовал эту функциональность, а подобная оптимизация в Babel (наиболее популярный JavaScript компилятор, используемый для компиляции ES6 в ES5), хоть и частичная, была позднее удалена.
Итого: рекурсия все еще может быть опасна в использовании для большого числа рекурсивных вызовов, даже если вы используете ее с осторожностью.
Что есть в JavaScript такого, чего нет в функциональных языках
Педанты скажут нам, что изменчивость объектов в JavaScript не самая большая проблема, и будут правы. Однако, иногда изменчивость и сайд-эффекты могут быть полезны (сайд-эффект — это изменение внешнего состояния, например глобальной переменной, внутри функции — прим. перев.). На самом деле, практически невозможно создать современное приложение без сайд-эффектов. Специализированные функциональные языки вроде Haskell используют сайд-эффекты, но маскируют их в чистых функциях используя специальные конструкции, называемые монады, позволяя программе оставаться чистой даже не смотря на сам факт присутствия сайд-эффектов в монадах.
Проблема с монадами в том, что несмотря на всю их простоту, объяснение того, что такое монада, кому-то, не знакомому с примерами ее использования, эквивалентно объяснению слепому от рождения человеку смысла слова «цвет».
«Монада — это моноид из категории эндофункторов, какие тут проблемы?» ~ Джеймс Айри, как бы цитирующий Филипа Вадлера, перефразируя реальную цитату Сондерса Мак Лейна. «Краткая, неполная и в большинстве своем неправильная история языков программирования»
Как правило, пародия преувеличивает смешные вещи, делая из еще забавнее. Цитата выше — это упрощенное определение термина монада, которое звучит примерно так:
«Монада в множестве Х это моноид из категории эндофункторов Х, в котором морфизм, называемый «произведение», заменен композицией эндофункторов, а морфизм, называемый «единица», заменен эндофунктором «тождественность». ~ Сандерс Мак Лейн. «Категории для практикующих математиков».
И даже так, по-моему мнению, страх перед монадами весьма слабое оправдание. Лучший способ изучить монады — это не прочитать стопку книг и пачку блог-постов, а просто совершить прыжок и начать использовать их. Как и большинство других вещей в функциональном программировании, головоломные академические определения понять сложнее, чем воспринять их идею. Поверьте, вовсе не обязательно понимать термины из книги Сандерса Мак Лейна, чтобы понимать функциональное программирование.
Хотя JavaScript может не быть идеальным вариантом для любого стиля программирования, он, безусловно, является языком общего назначения и спроектирован для использования разными людьми с их собственным опытом для различных целей.
Согласно Брендану Эйху, такая цель существовала с самого начала:
»… авторы компонентов, кто пишут на С++ или (мы надеемся) на Java, и «скриптовики», начинающие или «про», кто будут писать код, внедренный в HTML.»
Исходное намерение Netscape состояло в поддержке двух различных языков, и скриптовый язык, предположительно, должен был напоминать Scheme (диалект языка Lisp). Брендан Эйх:
«Я был нанят компанией Netscape с обещанием «реализовать Scheme» в браузере».
JavaScript должен был стать новым языком:
«Диктат высшего руководства заключался в том, что язык должен быть похож на Java. Это сразу оставило за бортом Perl, Python и Tcl вместе со Scheme.»
Таким образом, замысел Брендана Эйха с самого начала был:
Scheme в браузере
Выглядит как Java
Ну и кончилось это все даже большей мешаниной:
«Я совсем не горжусь, но счастлив, что выбрал в качестве основных ингредиентов scheme-подобные функции первого класса и self-подобные (хотя и единичные) прототипы (видимо, имеется ввиду, что в языке Self прототипы сложнее чем в JavaScript — прим. перев.). Влияние Java, особенно проблема y2k, а также разделение на примитивные типы и объекты, было неудачным.»
Я бы добавил к списку «неудачных» Java-подобных особенностей языка то, что в конце-концов вошло в JavaScript:
Функция-конструктор и ключевое слово
new
, с семантикой и способом вызова отличными от функций-фабрикКлючевое слово
class
вместе сextends
для наследования от единственного родителя как основной механизм наследованияТенденция разработчиков думать о классе как о статическом типе, коим он не является.
Мой вам совет: избегайте использования всего этого как только можете.
Мы счастливчики в том, что JavaScript стал настолько богатым языком, потому что в конечном итоге его скриптовый подход выиграл у «компонентного» подхода (на сегодня Java, Flash и ActiveX расширения не поддерживаются в большинстве используемых браузеров).
Мы пришли к тому, что единственным языком, напрямую поддерживаемым браузером стал JavaScript.
Это означает, что браузеры меньше перегружены и содержат меньше ошибок, потому что им нужно поддерживать только один язык — JavaScript. Вы можете подумать, что WebAssembly — это исключение, но одна из целей разработки WebAssembly — это использование существующей поддержки JavaScript абстрактным синтаксическим деревом (AST). На практике, первой демонстрацией возможностей WebAssembly стало подмножество JavaScript, известное как ASM.js.
Положение единственного стандартного языка программирования общего назначения для веба позволило JavaScript оседлать самую большую волну популярности в истории программного обеспечения:
Приложения съели мир, Интернет съел приложения, а JavaScript сожрал Интернет.
По многим параметрам JavaScript сейчас является самым популярным языком программирования в мире. JavaScript не идеален для функционального программирования, но это отличный инструмент для создания больших систем огромными распределенными командами, где каждая отдельная команда может иметь свои собственные представления о том, как писать код.
Некоторые команды могут сосредоточится на скриптовом подходе, в котором императивный стиль — это то, что нужно. Другие же могут сконцентрироваться на построении архитектуры абстракций, где применение (сдержанное, осторожное) принципов объектно-ориентированного подхода может стать отличной идеей. В то же время третьи могут попасть в объятия функционального подхода, редуцируя пользовательские действия с помощью чистых функций, создавая детерминированное, тестируемое управление состоянием приложения. Участники всех этих команд используют один и тот же язык, могут просто обмениваться идеями, учиться друг у друга, опираться на результаты работы друг друга.
В JavaScript все эти идеи могут сосуществовать одновременно, позволяя людям использовать язык, что привело к появлению наибольшего реестра ПО с открытым исходным кодом в мире npm
.
Истинная сила JavaScript — в разнообразии мнений и разработчиков в экосистеме. Возможно, это не совсем идеальный язык для сторонников функционального программирования, но он может быть идеальным языком для совместной работы, который работает практически на любой платформе, которую вы можете себе представить, — знакомым людям из других популярных языков, таких как Java, Lisp или C. JavaScript не будет идеален для разработчика из любого другого языка, но он может чувствовать себя достаточно комфортно для того, чтобы выучить язык и быстро стать продуктивным.
Я согласен с мнением, что JavaScript не лучший язык для ФП. Однако, ни один из других ФП языков не может похвастаться тем, что подходит каждому и каждый может принять его и начать использовать, а также, как продемонстрировал ES6 — JavaScript может становиться лучше и обслуживать нужды разработчиков, заинтересованных в ФП. Почему бы вместо забвения JavaScript и его удивительной экосистемы, используемой практически в каждой компании в мире, не принять его и постепенно не сделать его лучшим языком для создания ПО?
В текущем состоянии JavaScript достаточно неплох для ФП, разработчики могут создавать всевозможные виды полезных и интересных штук, используя техники функционального программирования. Netflix (и любое приложение на Anglular 2+) использует функциональные утилиты, основанные на библиотеке RxJS. Facebook использует концепт чистых функций, функций высшего порядка, и компонентов высшего порядка для разработки Facebook и Instagram. PayPal, KhanAcademy и Flipkart используют Redux для управления состоянием.
Они не одиноки: Angular, React, Redux и Lodash лидируют в рейтинге используемых фреймворков и библиотек в экосистеме JavaScript, и все они весьма серьёзно вдохновлены ФП, а в случае Lodash и Redux, спроектированы таким образом, чтобы продемонстрировать причины применения паттернов ФП в реальных JavaScript приложениях.
«Почему JavaScript?». Потому, что JavaScript это язык, который используют большинство реальных компаний для разработки реальных приложений. Вы можете любить или ненавидеть JavaScript за то, что он украл звание «наиболее популярного функционального языка» у Lisp, который нес это знамя десятилетия. Правда в том, что Haskell больше подходит на роль знаменосца функционального программирования сегодня, но люди пока просто не разрабатывают столько приложений на Haskell.
В любой момент времени в США открыто около ста тысяч вакансий и еще сотни тысяч по всему миру. Изучение Haskell научит вас многому из ФП, но изучение JavaScript научит вас многому из того, как создавать работающие приложения для реальной работы.
Приложения съели мир, Интернет съел приложения, JavaScript съел Интернет.