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

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

Это не про функции!

В программировании есть два больших подхода — императивное и функциональное. Они существенно отличаются логикой работы, ещё и создают путаницу в названиях. Сейчас объясним.

🤔 Функциональное — это про функции?

❌ Нет. Функциональное — это не про функции. Функции есть почти в любых языках программирования: и в функциональных, и в императивных. Отличие функционального программирования от императивного — в общем подходе.

Метафора: инструкция или книга правил

Представьте, что вы открываете кафе-столовую. Сейчас у вас там два типа сотрудников: повара и администраторы.

Для поваров вы пишете чёткие пошаговые инструкции для каждого блюда. Например:

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

Для администратора вы пишете не инструкцию, а как бы книгу правил:

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

❌ Программисты, не бомбите

Конечно же, это упрощено для понимания. Вы сами попробуйте это нормально объяснить (можно прямо в комментах).

Императивное программирование

Примеры языков: C, С++, Go, Pascal, Java, Python, Ruby

Императивное программирование устроено так:

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

Есть переменные, которые могут хранить данные и изменяться во время работы программы. Переменная — это ячейка для данных. Мы можем создать переменную нужного нам типа, положить туда какое-то значение, а потом поменять его на другое.

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

Получается, что в разные дни функция получает на вход 1000 ₽, но возвращает разные значения — так работает императивное программирование, когда всё зависит от других переменных.

Последовательность выполнения подпрограмм регулируется программистом. Он задаёт нужные условия, по которым движется программа. Вся логика полностью продумывается программистом — как он скажет, так и будет. Это значит, что разработчик может точно предсказать, в какой момент какой кусок кода выполнится — код получается предсказуемым, с понятной логикой работы.

Если у нас код, который считает скидку, должен вызываться только при финальном оформлении заказа, то он выполнится именно в этот момент. Он не посчитает скидку заранее и не пропустит момент оформления.

👉 Суть императивного программирования в том, что программист описывает чёткие шаги, которые должны привести код к нужной цели.

Звучит логично, и большинство программистов привыкли именно к такому поведению кода. Но функциональное программирование работает совершенно иначе.

Функциональное программирование

Примеры языков: Haskell, Lisp, Erlang, Clojure, F#

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

Например, в ООП нужно задать объекты и правила их взаимодействия между собой, но также можно и написать просто код, который не привязан к объектам. Он как бы стоит в стороне и влияет на работу программы в целом — отправляет одни объекты взаимодействовать с другими, обрабатывает какие-то результаты и так далее.

Функциональное программирование здесь идёт ещё дальше. В нём весь код — это правила работы с данными. Вы просто задаёте нужные правила, а код сам разбирается, как их применять.

Если мы сравним принципы функционального подхода с императивным, то единственное, что совпадёт, — и там, и там есть команды, которые язык может выполнять. Всё остальное — разное.

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

Переменных нет. Вернее, они есть, но не в том виде, к которому мы привыкли. В функциональном языке мы можем объявить переменную только один раз, и после этого значение переменной измениться не может. Это как константы — записали и всё, теперь можно только прочитать. Сами же промежуточные результаты хранятся в функциях — обратившись к нужной, вы всегда получите искомый результат.

Функции всегда возвращают одно и то же значение, если на вход поступают одни и те же данные. Если в прошлом примере мы отдавали в функцию сумму в 1000 ₽, а на выходе получали скидку в зависимости от дня недели, то в функциональном программировании если функция получит в качестве параметра 1000 ₽, то она всегда вернёт одну и ту же скидку независимо от других переменных.

Можно провести аналогию с математикой и синусами: синус 90 градусов всегда равен единице, в какой бы момент мы его ни посчитали или какие бы углы у нас ещё ни были в задаче. То же самое и здесь — всё предсказуемо и зависит только от входных параметров.

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

В русском языке всё работает точно так же: есть правила правописания и грамматики. Нам неважно, в каком порядке мы их изучили, главное — чтобы мы их вовремя применяли при написании текста или в устной речи. Например, мы можем сначала пройти правило «жи-ши», а потом правило про «не с глаголами», но применять мы их будем в том порядке, какой требуется в тексте.

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

Источник

Основы функционального программирования за 7 минут

Если вы не знаете, что такое функциональное программирование, но хотите писать чистый и поддерживаемый код, начните свое знакомство с ФП с этой статьи.

Чистота

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

Обратите внимание, что функция add никак не затрагивает переменную z. Она не читает значение z и ничего в нее не записывает. Она только принимает x и y и возвращает результат их сложения.

Функция add – чистая функция. Как только она будет работать со сторонними параметрами, например с переменной z – она больше не будет чистой.

Вот пример другой функции:

Кажется, что justTen будет относится к чистым функциям, но это не так. Она не принимает никаких параметров и просто возвращает константу. А если параметры функции не выполняют никакую работу, они не очень полезны. Поэтому правильней будет описать justTen просто как константу.

Полезная чистая функция должна принимать по крайней мере один параметр.

Разберем еще один пример.

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

Посмотрим, как будет работать функция add.

console.log(add(1, 2)) – всегда будет возвращать 3. Не большой сюрприз, конечно. Но это так, только потому, что add – чистая функция. Используй она в обработке параметров переменные извне и ее поведение стало бы непредсказуемым.

Чистая функция, которая получает на вход одни и те же параметры всегда будет возвращать одно и то же значение.

Побочные эффекты

Поскольку чистая функция не может взаимодействовать с внешними значениями, все следующие функции не будут являться чистыми:

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

Императивные языки программирования, такие как JavaScript, Java или C# имеют побочные эффекты всюду. Это делает отладку программы сложной, ведь переменные могут быть изменены в любом месте. И когда значение переменной изменится на неверное или сделает это не в то время, где придется искать ошибку? Везде? Это нехорошо.

На этом моменте может возникнуть вопрос – как же писать код используя только чистые функции? В функциональном программировании это необязательно.

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

Неизменяемость

Классический пример работы с переменными обычно не вызывает недоумения:

С математической точки зрения это неверно, ведь переменная x никогда не может быть равна x + 1. Но в императивном программировании это означает, что мы берем x увеличиваем ее значение на 1 и записываем новое значение обратно в переменную.

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

Ниже пример работы с константами-переменными в Elm, функциональном языке программирования для фронтенда.

addOneToSum здесь – функция, принимающая 2 параметра – y и z.

В блоке let переменная x получает значение 1, то есть теперь она равняется единице до конца своего существования.

В блоке in происходит вычисление x + y + z с использованием переменных, объявленных в let, а именно x. В итоге функция вернет значение 1 + y + z.

Кажется, что без переменных в классическом понимании работать невозможно, но давайте разберемся, когда нам нужно модифицировать значение переменной. Два общих случая вспоминаются сразу: изменения хранимого значения объекта или записи и счетчик цикла.

С первым случаем в функциональном программировании разбираются путем создания копии записи с измененным значением. Для эффективного управления копиями используются соответствующие структуры данных.

А циклов в привычном функциональном программировании попросту нет. Это значит, что такие конструкции как for, while, do, repeat – не используются. Для создания циклов в функциональном программировании используется рекурсия.

Циклы в функциональном программировании

В JavaScript есть два способа создания циклов:

Во втором случае не изменяются старые значения, вместо этого используются новые значения, посчитанные из старых.

В языке Elm такие конструкции проще читать и понимать:

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

Читайте также:

Функциональное программирование: рефакторинг, замыкания и функции высшего порядка

Источник

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

Эта статья является переводом материала «What is functional programming?».

В этой статье Владимир Хориков попытается ответить на вопрос: что такое функциональное программирование?

Функциональное программирование

Математические функции не являются методами в программном смысле. Хотя мы иногда используем слова «метод» и «функция» как синонимы, с точки зрения функционального программирования это разные понятия. Математическую функцию лучше всего рассматривать как канал (pipe), преобразующий любое значение, которое мы передаем, в другое значение:

Вот и все. Математическая функция не оставляет во внешнем мире никаких следов своего существования. Она делает только одно: находит соответствующий объект для каждого объекта, который мы ему скармливаем.

Для того чтобы метод стал математической функцией, он должен соответствовать двум требованиям. Прежде всего, он должен быть ссылочно прозрачным (referentially transparent). Ссылочно прозрачная функция всегда дает один и тот же результат, если вы предоставляете ей одни и те же аргументы. Это означает, что такая функция должна работать только со значениями, которые мы передаем, она не должна ссылаться на глобальное состояние.

Этот метод не является ссылочно прозрачным, потому что он возвращает разные результаты, даже если мы передаем в него один и тот же год. Причина здесь в том, что он ссылается на глобальное свойство DatetTime.Now.

Ссылочно прозрачной альтернативой этому методу может быть (Эта версия работает только с переданными параметрами):

Во-вторых, сигнатура математической функции должна передавать всю информацию о возможных входных значениях, которые она принимает, и о возможных результатах, которые она может дать. Можно называть эту черту честность сигнатуры метода (method signature honesty).

Посмотрите на этот пример кода:

Метод Divide, несмотря на то, что он ссылочно прозрачный, не является математической функцией. В его сигнатуре указано, что он принимает любые два целых числа и возвращает другое целое число. Но что произойдет, если мы передадим ему 1 и 0 в качестве входных параметров?

Вместо того, чтобы вернуть целое число, как мы ожидали, он вызовет исключение «Divide By Zero». Это означает, что сигнатура метода не передает достаточно информации о результате операции. Он обманывает вызывающего, делая вид, что может обрабатывать любые два параметра целочисленного типа, тогда как на практике он имеет особый случай, который не может быть обработан.

Чтобы преобразовать метод в математическую функцию, нам нужно изменить тип параметра «y», например:

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

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

Побочные эффекты (Side effects)

Сигнатура метода с побочным эффектом не передает достаточно информации о фактическом результате операции. Чтобы проверить свои предположения относительно кода, который вы пишете, вам нужно не только взглянуть на саму сигнатуру метода, но также необходимо перейти к деталям его реализации и посмотреть, оставляет ли этот метод какие-либо побочные эффекты, которых вы не ожидали:

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

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

Исключения

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

Примитивная одержимость (Primitive Obsession)

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

Что нам говорит сигнатура метода CreateUser? Она говорит, что для любой входной строки он возвращает экземпляр User. Однако на практике он принимает только строки, отформатированные определенным образом, и выдает исключения, если это не так. Следовательно, этот метод нечестен, поскольку не передает достаточно информации о типах строк, с которыми работает.

По сути, это та же проблема, которую вы видели с методом Divide:

Тип параметра для электронной почты, а также тип параметра для «y» являются более грубыми, чем фактическая концепция, которую они представляют. Количество состояний, в которых может находиться экземпляр строкового типа, превышает количество допустимых состояний для правильно отформатированного электронного письма. Это несоответствие приводит к обману разработчика, который использует такой метод. Это заставляет программиста думать, что метод работает с примитивными строками, тогда как на самом деле эта строка представляет концепцию предметной области со своими инвариантами.

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

Nulls

Но тут, конечно, зависит от языка. Автор оригинала работает с C#, в котором до 8 версии нельзя было указывать является ли значение nullable (https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/nullable-value-types). Так как оригинал статьи 2016 года, на тот момент еще не было такой возможности в C#.

В некоторых случаях это именно то, что вам нужно, но иногда вы хотите просто вернуть MyClass без возможности его преобразования в null. Проблема в том, что в C # это невозможно сделать. Невозможно различить ссылочные типы, допускающие значение NULL, и ссылочные типы, не допускающие значения NULL. Это означает, что методы со ссылочными типами в своей сигнатуре по своей сути нечестны.

Эту проблему можно решить, введя тип Maybe и соглашение внутри команды о том, что всякий раз, когда вы определяете переменную, допускающую значение NULL, вы используете для этого тип Maybe.

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

Важный вопрос, который приходит на ум, когда вы читаете о функциональном программировании: зачем вообще беспокоиться об этом?

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

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

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

Резюме

Практики, которые помогают преобразовать методы в математические функции:

Избегать исключения для управления потоком программы.

Источник

Шпаргалка по функциональному программированию

Привет, меня зовут Григорий Бизюкин, я преподаватель Школы разработки интерфейсов и фронтенд-разработчик в Яндексе. Давайте поговорим о функциональном программировании в мире JavaScript. Мы все про ФП что-то слышали, нам всем оно интересно, но у меня, когда я искал полезные материалы для подготовки к лекциям, сложилось такое впечатление: есть куча статей, каждая из которых либо говорит об ФП общими словами, либо раскрывает отдельный маленький кусочек темы, чего, конечно, недостаточно.

Добавим функционального света

Впервые я попробовал обобщить в одном месте самые популярные и, как мне кажется, применимые приёмы функционального программирования в лекции для ШРИ. Потом захотелось расширить материал и рассмотреть ещё больше концепций. В результате получилась эта статья. В ней мы разберём всё самое сложное простым языком с понятными примерами. Надеюсь, вам будет интересно!

Функциональное программирование

Когда кто-то рассуждает о функциональном программировании, речь может идти как о парадигме целиком, так и об отдельных подходах, таких как чистые функции, неизменяемые данные и другие.

Важно понимать, что когда на глаза попадается очередная статья «Почему разработчик обязательно должен знать ФП», то автор, вероятно, говорит именно о нескольких подходах из мира ФП, которые можно применить у себя на проекте, чем о том, что вам пора пересесть за Haskell.

Функциональное программирование — штука интересная, но вряд ли вы захотите переписать весь проект на функциональном языке. На практике именно отдельные подходы оказываются самыми полезными и применимыми. На них и сконцентрируемся. В контексте ФП часто можно встретить термины вроде линз и монад. Здесь они останутся за скобками, потому что уж слишком специфичны.

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

За и против

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

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

Императивный vs декларативный

Императивный подход говорит о том, как решать задачу, декларативный — что хотим получить в результате.

В жизни мы, как правило, думаем о результате. Например, когда просим маму приготовить пиццу, делаем это декларативно: «Мам, хочу пиццу!» Если бы мы делали это императивно, то получилось бы что-то вроде: «Мама, открой холодильник, достань тесто, возьми помидоры» и так далее.

В разработке та же история. Когда мы пишем декларативно, код выглядит гораздо проще:

Для фильтрации массива чисел больше не нужно думать о деталях: о том, как инкрементировать переменную i и как не выйти за границы массива. Нам достаточно передать предикат Boolean в функцию filter, и дело сделано.

Причём если вам кажется, что декларативный стиль — нечто новенькое, то спешу вас заверить — это не так. Вы наверняка писали css-стили, где говорили, что именно хотите получить в результате:

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

Такая же история и с SQL:

Запрос говорит о результате, а не о том, как именно его выполнить.

Функции и процедуры

Функция — понятие, близкое к математическому. Она что-то получает на вход и всегда что-то возвращает.

Процедура, в свою очередь, вызывается ради побочных эффектов:

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

В JS не существует процедур, потому что то, что мы считаем процедурой, на самом деле является функцией без return. Если опустить return, функция всё равно неявно возвращает undefined и остаётся функцией.

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

Параметры и аргументы

Параметры — это переменные, созданные в объявлении функции. Аргументы — конкретные значения, переданные при вызове.

Сигнатура

Количество, тип и порядок параметров. Объявление функции в JS не содержит информации о типе параметров из-за динамической типизации. Если не используется TypeScript, эту информацию можно указать через JSDoc.

Арность

Арность — количество параметров, которые принимает функция. В JavaScript арность функции можно определить при помощи свойства length.

У свойства length есть особенности, которые следует учитывать:

Рекурсия

Когда функция вызывает саму себя, происходит рекурсивный вызов. Для его корректной работы необходимо, чтобы внутри функции было хотя бы одно рекурсивное условие, на которое мы обязательно рано или поздно выйдем. Если этого не произойдёт, программа зациклится.

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

Несмотря на заманчивые возможности, поддержка хвостовой рекурсии до сих пор отсутствует и вряд ли появится в будущем, поэтому сведения о ней носят чисто теоретический характер. Если добавить в самое начало функции console.trace, можно убедиться, что каждый новый вызов создаёт новый кадр в стеке, несмотря на то, что условия рекурсии выполняются. Более подробно об оптимизации хвостовых вызовов можно почитать здесь.

Функция первого класса

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

Функция высшего порядка

Функции, которые принимают или возвращают другие функции. С ними мы работаем каждый день.

При этом высшим порядком могут быть не только функции, но и, например, компоненты в React, принимающие или возвращающие другие компоненты. Они, соответственно, называются компонентами высшего порядка.

Предикат

Это функция, которая возвращает логическое значение. Самый распространённый пример — использование предиката внутри функций filter, some, every.

Замыкание

Замыкание — это функция плюс её область видимости. Замыкание создаётся заново каждый раз при вызове функции и позволяет получить значение к переменным, объявленным во внешней функции.

В примере внутри замыкания хранятся две переменные: tag и count. Каждый раз, когда мы создаём новую переменную внутри другой функции и возвращаем её наружу, функция находит переменную, объявленную во внешней функции, через замыкание. Если тема замыканий кажется чем-то загадочным, почитайте о них подробнее в блоге HTML Academy.

Мемоизация

Полезный приём — функция кеширует результаты своего вызова:

Можно заметить, как одни возможности становятся базой для других, более сложных. Благодаря функциям первого класса становятся возможны функции высших порядков, благодаря которым становятся возможны замыкания. А благодаря замыканиям становится возможной мемоизация.

Конвейер и композиция

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

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

Конвейер

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

Возможно, в JavaScript тоже появится нечто похожее. В одном из предложений для ESNext описывается конвейерный оператор, при помощи которого можно будет делать вот такие штуки:

Если бы у нас была функция pipe, которая аналогичным образом организовывает поток данных, через неё можно было бы записать так:

Аргумент, переданный в конвейер, последовательно проходит слева направо:

Хм, а что если запустить конвейер в другую сторону?

Композиция

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

Если записать то же самое через вспомогательную функцию compose, получится:

Внешне всё осталось почти так же, но место вызова функции increment изменилось, потому что теперь цепочка вычислений стала работать справа налево:

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

Таким образом, конвейер и композиция — это два направления одного потока данных.

Преимущества

Когда поток данных организован через вспомогательные функции pipe или compose, больше не нужно писать много скобок, кроме того, код выглядит более декларативным, то есть более читаемым. Но есть ещё два момента, которые можно легко упустить.

Создание новых абстракций

Функции чем-то похожи на кубики Лего. Когда мы строим программу, она состоит из отдельных кубиков, причём каждый из них решает свою задачу.

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

Например, у нас есть два готовых кубика: получить слова из текста, оставить только уникальные слова:

Затем мы замечаем, что хотим переиспользовать возможности двух кубиков, и создаём новую деталь. Для этого мы строим новую абстракцию — оборачиваем последовательный вызов двух функций в новую функцию, которая и станет нашей новой деталью.

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

Когда мы решим переиспользовать эту деталь и создать на её основе ещё одну более сложную сущность, композиция запросто с этим справится.

В примере ниже мы берём ранее созданную деталь и делаем новую функцию, которая будет не только находить уникальные слова, но ещё и сортировать их по алфавиту.

Если речь идёт о конструировании сложных деталей, вложенную композицию можно заменить на линейную:

Таким образом, композиция — это не просто шаблон для организации потока вычислений, но и фабрика по производству новых деталей.

Бесточечный стиль

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

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

При работе со стилем без параметров функция не упоминает данные, которые мы обрабатываем.

В разработке есть две по-настоящему сложные проблемы: инвалидировать кеш и придумать названия для переменных. Композия не поможет решить задачу с кешем, но проблем с именованием параметров точно станет меньше.

Ограничения

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

Здесь на помощь приходит частичное применение и каррирование, о которых мы поговорим позже.

Пишем сами

Реализовать конвейер можно было бы так:

Чтобы реализовать композицию, достаточно заменить reduce на reduceRight:

Как на практике?

На практике не так много случаев, где можно применить композицию. Кроме того, применимость ограничена отсутствием в JS встроенных механизмов — нужно использовать библиотеки или самостоятельно реализовывать у себя необходимые функции. Но при этом, как ни странно, если вы используете на проекте Redux, ничего подключать не придётся, потому что композиция входит в состав библиотеки.

На проекте с Redux композиция наверняка будет использоваться для middleware, потому что createStore принимает только один усилитель (enhancer), а их, как правило, требуется хотя бы несколько.

Мы помним, что композиция направлена справа налево. Промежуточные слои, которые должны быть вызваны раньше других, помещаются правее или ниже. В примере выше DevTools добавляются до применения middleware, чтобы можно было корректно дебажить асинхронный код.

Другой кейс, где может пригодиться композиция — фильтрация или преобразование потока данных:

Частичное применение и каррирование

Преобразуют функцию с исходным набором параметров в другую функцию с меньшим числом параметров, но делают это по-разному.

Для демонстрации работы частичного применения и каррирования будем использовать такую незамысловатую функцию:

Частичное применение

Преобразует функцию в одну функцию с меньшим числом параметров.

Каррирование

Преобразует функцию в набор функций с единственным параметром.

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

В чём разница?

Как мы уже выяснили, частичное применение преобразовывает функцию в одну единственную, в то время как каррирование преобразовывает её в набор функций. Это означает, что когда мы передаём аргументы в количестве меньшем, чем арность функции, при частичном применении происходит вызов функции:

В то время как каррирование будет возвращать новые функции до тех пор, пока не наберётся достаточное число аргументов.

Можно провести аналогию: вы делаете заказ в ресторане. Если использовать частичное применение, официант задаст вам только один вопрос о том, что вы хотите заказать, и если вы ответите «хочу пиццу», то остальное он додумает сам и принесёт ту пиццу, которую посчитает нужной.

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

Решение задачи с композицией

Проблему с композицией из предыдущего примера при помощи частичного применения или каррирования можно решить вот так:

Порядок данных

Частичное применение и каррирование чувствительны к порядку данных. Существует два подхода к порядку объявления параметров.

В обычном проекте вы вряд ли будете писать функции, где более специфические данные следует передавать в первую очередь, поэтому полезно держать под рукой хелпер для преобразования iterate-last в iterate-first. Его можно написать и применить вот так:

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

Специализация

Кроме применения в композиции для настройки сигнатуры функции, у частичного применения и каррирования есть другая полезная особенность. С их помощью можно делать функции более специфичными. Например, мы хотим сделать запрос API:

Затем понимаем, что хотим переиспользовать функцию для запроса данных с определённого адреса. В этом случае мы точно так же, как создавали детали через композицию, можем создать новую, но на этот раз более специфическую деталь при помощи каррирования или частичного применения.

Пишем сами

Свою версию частичного применения можно написать примерно так:

Каррирование выглядит немного сложнее:

Как на практике?

Две основные возможности частичного применения и каррирования: настройка функций для реализации композиции и специализация. Композиция используется редко, поэтому специализация является гораздо более полезной.

Неизменяемые данные

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

Для иллюстрации принципа работы неизменяемых данных подойдёт пример со стаканом. Представим, что у нас есть стакан с водой, из которого мы немного выпиваем, а через некоторое время делаем ещё один глоток. Стакан опустеет ровно настолько, сколько мы из него выпили. Это — изменяемые данные.

С неизменяемыми структурами данных совершенно другая история. Перед тем, как сделать глоток, создаётся точная копия стакана, и мы пьём уже из копии. Таким образом, после первого глотка у нас будет два стакана: один полный, из другого мы немного отпили. Исходный стакан останется без изменений. Это и есть неизменяемые данные.

Преимущества неизменяемых структур данных:
— предсказуемое изменение состояния,
— быстрое сравнение по ссылке,
— кешируемость,
— легко распараллеливать,
— легко тестировать.

Но у неизменяемых структур есть два больших недостатка: нужно помнить о том, что данные надо копировать, когда это необходимо, и, соответственно, появляются затраты на копирование. Рассмотрим их подробнее.

Нечаянное мутирование данных

В JavaScript запросто можно нечаянно мутировать массив или любой другой объект:

Мы можем попробовать защититься от этого, но есть проблема. Вещи, которые кажутся неизменяемыми, на самом деле таковыми не являются. Объявление через const защищает от изменений только ссылку, а сам объект остаётся открыт для мутаций.

Все ссылочные типы — объекты, массивы и другие — всегда передаются по ссылке. Во время присваивания или передачи параметра происходит копирование ссылки, но не самих данных.

А что если применить средства метапрограммирования и, например, заморозить объект? В этом случае мы всё равно сможем изменить вложенные объекты по ссылке.

Вместо заморозки можно воспользоваться Proxy API, но в этом случае тоже придётся дополнительно обрабатывать вложенные структуры, потому что они всё ещё открыты для изменений.

В общем, в JavaScript нельзя просто так взять и защитить данные от непреднамеренного изменения.

Затраты на копирование

С копированием данных тоже не всё так просто. В большинстве случаев работает копирование массивов и объектов встроенными средствами JavaScript:

К сожалению, в этом случае создаётся поверхностная копия, поэтому мы избавляемся от мутаций только до тех пор, пока отсутствуют другие вложенные объекты:

Такая же история с функциональными методами массивов — map и filter создают поверхностную копию исходного массива.

Поэтому для создания полноценной копии нужна встроенная функция глубокого копирования, которая потребует дополнительных затрат. Реализовать глубокое копирование можно несколькими способами, подробнее о возможных вариантах читайте здесь:

Неизменяемые структуры данных (persistent data structures)

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

Пожалуй, две самые популярные библиотеки в мире фронтенд-разработки — это Immutable и Immer. При помощи Immer мы можем сделать вот что:

Да, нам всё равно приходится для изменения данных вызывать функцию produce, но это уже лучше, чем рассчитывать на отсутствие случайных мутаций. Кроме того, Immer замораживает все объекты, которые возвращает produce, чтобы защитить разработчика от возможных нечаянных мутаций.

Как на практике?

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

Но, как мне кажется, во многих других ситуациях неизменяемые структуры данных подойдут куда лучше:

Чистые функции (pure functions)

Функции без побочных эффектов, которые зависят только от параметров и для одних и тех же аргументов всегда возвращают один и тот же результат.

Чистые функции могут вызывать другие чистые функции, но если внутри цепочки вызовов попадётся хотя бы одна функция, которая не отвечает условиям чистоты, вся цепочка перестаёт быть чистой. Рассмотрим подробно каждое из условий, которым должны отвечать чистые функции.

Побочные эффекты (side effects)

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

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

Работа с глобальными переменными — тоже побочный эффект.

Как правило, изменение глобальных значений непосредственно влияет на текущее состояние приложения, в то время как операции ввода/вывода меняют что-то за пределами приложения. Но в веб-разработке всё вращается вокруг DOM. Это не только доступы и изменение глобальных переменных, но ещё и операции ввода/вывода. Получается, что фронтенд — один сплошной побочный эффект. Другими словами, фронтенд замечателен тем, что совмещает в себе всё «самое лучшее».

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

Мутация данных внутри функции — ещё одна разновидность побочных эффектов. Функция, которая мутирует данные, как бы оставляет след в виде изменений после вызова. Сложность в том, что многие встроенные функции JS по умолчанию мутируют данные. Если об этом забыть, можно нечаянно оставить после вызова функции след из побочных эффектов.

Лучший способ избежать мутации данных — использовать неизменяемые структуры данных.

Зависимость от параметров

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

Внешние зависимости можно заменить на зависимость от параметров.

Непредсказуемый результат

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

Чтобы сделать функцию чистой, достаточно вынести неопределённость за пределы функции и передать её в качестве параметра. Например, вот так:

Теперь функция всегда возвращает один и тот же результат для одних и тех же параметров.

Преимущества чистых функций

Их плюсы:
— проще разобраться, как устроена функция,
— их можно запросто кешировать,
— легко тестировать,
— легко распараллеливать.

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

Так почему бы всё не написать на чистых функциях?

Абсолютная и относительная чистота

Если взять и написать программу только из чистых функций, то получится:

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

Кроме того, чистота относительна. Функция ниже — чистая или нет?

Строго говоря, такая функция не является чистой, потому что зависит от глобальной переменной, но вряд ли кому-то захочется менять значение PI, поэтому не стоит доводить погоню за чистотой до абсурда.

Заключение

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

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

Кроме того, загляните в репозиторий Awesome FP JS, вдруг найдёте что-то интересное для себя. Если же захочется целиком погрузиться в функциональную парадигму, но при этом продолжать разрабатывать фронтенд, можно посмотреть в сторону Ramda или Elm.

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

Источник

Понравилась статья? Поделиться с друзьями:

Не пропустите наши новые статьи:

  • что такое функциональное программирование js
  • что такое фулбоди программа тренировок
  • что такое фронт в программировании
  • Что такое фреймворки в языках программирования
  • что такое фреймворки в программировании

  • Операционные системы и программное обеспечение
    0 0 голоса
    Рейтинг статьи
    Подписаться
    Уведомить о
    guest
    0 комментариев
    Старые
    Новые Популярные
    Межтекстовые Отзывы
    Посмотреть все комментарии