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

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

Это нефункциональная функция:

Это функциональная функция:

Не перебирайте списки. Используйте карту и уменьшите.

карта

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

Это простая карта, которая принимает список имен и возвращает список длин этих имен:

Это карта, которая возводит в квадрат каждое число в переданной коллекции:

Эта карта не принимает именованных функций. Для этого требуется анонимная встроенная функция, известная как функция стрелки в JavaScript (иногда называемая лямбда в более широком функциональном контексте). Параметры функции определены слева от стрелки. Тело функции определяется справа от стрелки. Результат выполнения тела функции возвращается (неявно).

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

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

Это можно переписать как карту:

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

(Надеюсь, секретные агенты сохранят хорошие воспоминания и не забудут секретные кодовые имена друг друга во время секретной миссии.)

Мое решение:

Уменьшать

Reduce принимает функцию и набор элементов. Он возвращает значение, созданное путем объединения элементов.

Это простое сокращение. Он возвращает сумму всех элементов в коллекции.

x - это текущий элемент, по которому выполняется итерация. a - аккумулятор. Это значение, возвращаемое выполнением лямбда-выражения для предыдущего элемента. reduce() проходит по предметам. Для каждого из них он запускает лямбду для текущих a и x и возвращает результат как a следующей итерации.

Что такое a в первой итерации? Для него нет результата предыдущей итерации. reduce() использует первый элемент в коллекции для a в первой итерации и начинает итерацию со второго элемента. То есть первый x - это второй элемент.

Этот код подсчитывает, как часто слово «Сэм» появляется в списке строк:

Это тот же код, написанный как сокращение:

Как этот код приходит к своему начальному a? Отправной точкой для количества случаев появления «Сэма» не может быть «Мэри прочитала сказку Сэму и Исле». Начальный аккумулятор указывается вторым аргументом reduce(). Это позволяет использовать значение другого типа, нежели элементы в коллекции.

Почему карта и сокращение лучше?

Во-первых, они часто однострочные.

Во-вторых, важные части итерации - сбор, операция и возвращаемое значение - всегда находятся в одних и тех же местах на каждой карте и сокращаются.

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

В-четвертых, map и reduce - элементарные операции. Каждый раз, когда человек читает цикл for, он должен пройти логику построчно. Есть несколько структурных закономерностей, которые они могут использовать для создания каркаса, на котором будет зависеть их понимание кода. Напротив, map и reduce - это одновременно строительные блоки, которые можно объединить в сложные алгоритмы, и элементы, которые читатель кода может мгновенно понять и абстрагироваться в уме. «Ах, этот код преобразует каждый элемент в этой коллекции. Это отбрасывает некоторые преобразования. Он объединяет остаток в единый выход ».

В-пятых, у map и reduce есть много друзей, которые предоставляют полезные, измененные версии своего базового поведения. Например: фильтр, каждый, некоторые и найти.

Упражнение 2. Попробуйте переписать приведенный ниже код, используя map, reduce и filter. Фильтр принимает функцию и возвращает коллекцию всех элементов, для которых функция вернула true.

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

Мое решение:

Пишите декларативно, а не повелительно

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

Это пример вывода:

 -
 --
 --
 --
 --
 ---
 ---
 --
 ---
 ----
 ---
 ----
 ----
 ----
 -----

Это программа:

Код написан императивно. Функциональная версия будет декларативной. Он описывал бы, что делать, а не как это делать.

Используйте функции

Программу можно сделать более декларативной, объединив фрагменты кода в функции.

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

Комментариев больше нет. Код описывает себя.

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

Удалить состояние

Это функциональная версия кода автогонок:

Код по-прежнему разделен на функции, но функции остаются функциональными. Тому есть три признака. Во-первых, больше нет общих переменных. time и carPositions проходят прямо в race(). Во-вторых, функции принимают параметры. В-третьих, внутри функций не создаются экземпляры переменных. Все изменения данных производятся с возвращаемыми значениями. race() повторяется с результатом runStepOfRace(). Каждый раз, когда шаг генерирует новое состояние, он немедленно передается на следующий шаг.

Итак, вот две функции, zero() и one():

zero() принимает строку, s. Если первый символ '0', он возвращает оставшуюся часть строки. Если это не так, возвращается null. one() делает то же самое, но для первого символа '1'.

Представьте себе функцию с именем ruleSequence(). Он принимает строку и список функций-правил в форме zero() и one(). Он вызывает первое правило в строке. Если не возвращается null, он принимает возвращаемое значение и вызывает для него второе правило. Если не возвращается null, он принимает возвращаемое значение и вызывает для него третье правило. И так далее. Если какое-либо правило возвращает null, ruleSequence() останавливается и возвращает null. В противном случае он возвращает возвращаемое значение окончательного правила.

Это пример ввода и вывода:

Это императивная версия ruleSequence():

Упражнение 3. В приведенном выше коде для работы используется цикл. Сделайте его более декларативным, переписав его как рекурсию.

Мое решение:

Используйте конвейеры

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

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

Беспокойство вызывает название функции. «Формат» очень расплывчатый. При ближайшем рассмотрении кода эти опасения начинают исчезать. В одном цикле происходят три вещи. Значение ключа 'country' устанавливается на 'Canada'. В названии группы убрана пунктуация. Название группы пишется с заглавной буквы. Трудно сказать, для чего предназначен код, и трудно сказать, делает ли он то, что кажется. Код сложно использовать повторно, сложно тестировать и распараллеливать.

Сравните это с этим:

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

Задача pipelineEach() - передавать бэнды по одной в функцию преобразования, например setCanadaAsCountry(). После применения функции ко всем бэндам pipelineEach() объединяет преобразованные бэнды. Затем он передает каждую следующую функцию.

Давайте посмотрим на функции преобразования.

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

Вроде все нормально. Оригиналы словаря бэндов защищены от изменения, когда ключ связывается с новым значением. Но в приведенном выше коде есть и другие возможные мутации. В stripPunctuationFromName() имя без знаков препинания генерируется путем вызова replace() исходного имени. Если replace() не работает, stripPunctuationFromName() тоже.

К счастью, replace() не изменяет строки, с которыми работает. Это потому, что строки неизменяемы в JavaScript. Когда replace() работает со строкой имени диапазона, исходное имя диапазона копируется, и на копии вызывается replace(). Уф.

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

Упражнение 4. Попробуйте написать функцию pipelineEach. Подумайте о порядке действий. Бэнды в массиве передаются по одному бэнду за раз первой функции преобразования. Полосы в результирующем массиве передаются по одной полосе во вторую функцию преобразования. И так далее.

Мое решение:

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

Или, если мы готовы пожертвовать удобочитаемостью ради краткости, просто:

Код для call():

Здесь много чего происходит. Давайте по частям.

Один. call() - функция высшего порядка. Функция более высокого порядка принимает функцию в качестве аргумента или возвращает функцию. Или, как call(), делает и то, и другое.

Два. applyFn() очень похож на три функции преобразования. Требуется пластинка (группа). Он ищет значение в record[key]. Он вызывает fn для этого значения. Он присваивает результат копии записи. Возвращает копию.

Три. call() не выполняет никакой работы. applyFn(), когда его вызовут, сделает всю работу. В приведенном выше примере использования pipelineEach() один экземпляр applyFn() установит 'country' в 'Canada' на переданном диапазоне. Другой экземпляр будет использовать имя переданного диапазона с заглавной буквы.

Четыре. Когда запущен экземпляр applyFn(), fn и key не попадают в область действия. Они не аргументы applyFn() и не местные жители внутри него. Но они все равно будут доступны. Когда функция определена, она сохраняет ссылки на переменные, которые она закрывает: те, которые были определены в области вне функции и которые используются внутри функции. Когда функция запущена и ее код ссылается на переменную, JavaScript ищет переменную в локальных переменных и в аргументах. Если он не находит его там, он ищет в сохраненных ссылках закрытые переменные. Здесь он найдет fn и key.

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

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

Остается сделать еще одну часть обработки бэндов. То есть удалить все, кроме названия и страны. extractNameAndCountry() может извлечь эту информацию:

extractNameAndCountry() можно было бы записать как общую функцию с именем pluck(), которую можно было бы использовать следующим образом:

Упражнение 5. pluck() берет список ключей для извлечения из каждой записи. Попробуй и напиши. Это должна быть функция более высокого порядка.

Мое решение:

Что теперь?

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

Подумайте о Мэри, Исле и Сэме. Превратите итерации списков в карты и уменьшите.

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

Подумайте о группах. Превратите последовательность операций в конвейер.

Спасибо Мэри Роуз Кук за написание Практического введения в функциональное программирование. Статья воспроизведена и адаптирована здесь с разрешения автора. Также благодарим Agustín за тщательный анализ статьи и Álex, Jesús, Israel и Santiago за просмотр примеров кода.