Что такое чистая функция js

от admin

Существуют ли чистые функции в JavaScript?

Недавно я ввязался в дискуссию о том, как определить чистую функцию в JavaScript. Вся концепция чистоты, кажется, расплывается в таком динамичном языке. Следующие примеры показывают, что нам, возможно, придётся пересмотреть термин «чистая функция», или, как минимум, быть очень осторожными, когда мы его используем.

Что такое чистая функция?

Если вы не знакомы с этим термином, то я рекомендую вам сначала прочитать небольшое вступление. Определение «чистая функция» от Alvin Alexander и Мастер JavaScript интервью: что такое чистая функция? от Eric Elliott будут отличным выбором.

Вкратце, функция называется чистой, если она удовлетворяет двум условиям:

  1. Функция возвращает точно такой же результат каждый раз, когда она вызывается с тем же набором аргументов.
  2. Выполнение функции не изменяет какое-либо состояние за пределами её области видимости и не оказывает видимого воздействия на внешний мир, кроме возвращения значения (никаких побочных эффектов).

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

Что из этого является чистым?

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

Сделали? Отлично, давайте сравним.

Когда я спрашивал, подавляющее большинство ответило, что функция doubleB является единственной нечистой, а функции doubleA , doubleC и doubleD чисты.

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

Первое более интересное. При вызове с теми же аргументами все они возвращают одно и то же значение (используем toEqual для поддержки массивов):

Нуууу, написано так, что да. Однако, как насчет этой части кода, опубликованного моим другом Alexander?

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

Это заставило меня задуматься. Если это подтвердилось, то что насчёт других? Выдержат ли они, если я достаточно постараюсь? Как вы догадались, нет, они этого не сделают. Фактически, я сейчас говорю:

Ни одна из четырёх функций не является чистой.

Функции как объекты первого класса

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

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

Map по массиву

«Map, filter, reduce. Повторить». Это было название одной из моих livecoding-сессий. Эти три метода — ядро преобразования данных в функциональном программировании. Поэтому они должны быть безопасными для использования в чистых функциях.

Как выясняется, в JavaScript ничто не высечено в камне. Или я должен сказать в прототипе?

Подождите. Это, конечно, недопустимо. Это неправильно.

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

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

Поэтому функция doubleD не является чистой.

Умножение чисто

Однако в JavaScript нельзя динамически переопределять встроенные операторы, как в некоторых языках.

Кроме того, n — локальная переменная, живущая только в области видимости этой функции. Её невозможно изменить извне.

Нет, это действительно невозможно. Вы должно быть невысокого мнения о JavaScript, если у вас есть на это надежда ��.

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

Хоть я и не могу изменить операцию или аргумент после его передачи, у меня есть свобода выбора того, что можно передать. Числа, строки, булевые значения, объекты…

Объекты? Какое применение у них может быть? Число, помноженное на объект, равно, эм… как 2 * <> . NaN . Проверьте это в консоли. Как это сделал я.

Хотя это не поможет. Если бы только был способ заставить среду выполнения преобразовать объект в число при умножении.

toString для чисел

Если объект появляется в контексте строки, например в случае конкатенации со строкой, движок запускает функцию toString объекта и использует результат. Если функция не реализована, он будет возвращаться к известному '[object Object]' , созданному методом Object.prototype.toString .

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

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

Примечание: в предыдущей версии этой статьи использовался @@toPrimitive или, более точно, Symbol.toPrimitive . Как отметил Alexandre Morgaut, valueOf достаточно и он поддерживается с первой версии JavaScript. Если вы не знакомы с @@toPrimitive , вы всё ещё можете прочитать здесь.

И все-таки: что такое чистая функция?

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

Я хочу, чтобы все четыре функции были чистыми, если я так решил. Да, включая функции типа doubleB . Что делать, если эта переменная (в нашем случае, two ) не может изменяться, например это математическая константа e , pi или phi? Она должна быть чистой.

Я хочу иметь возможность доверять встроенным функциям. Какие программы я могу создать, если я предполагаю, что всё что угодно в Array.prototype или Object.prototype может измениться? Всё просто: никто никогда не захочет их использовать.

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

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

У вас есть идеи для определения? Как вы решаете, что функция является чистой? Есть что-нибудь, что я упустил? Вы чему-нибудь научились?

Примечание

Есть несколько способов защиты от некоторых трюков, использованных выше.

Переопределение свободной переменной типа two или getTwo можно избежать, инкапсулируя весь блок в функцию. Либо использовать IIFE или модули:

Лучшим решением было бы использование const, представленного в ES2015:

Предотвращение от злоупотребления valueOf или @@toPrimitive также возможно, но громоздко. Например, вот так:

Можно было обойти трюк с изменением Array.prototype , только избегая таких функций и возвращаясь к циклам for (for . of) . Это уродливо, непрактично и потенциально невозможно. Абстрагирование этого или использование библиотеки имеет свои недостатки.

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

Что такое чистые функции в JavaScript?

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

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

Чек-лист

Функция должна удовлетворять двум условиям, чтобы считаться «чистой»:

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

— Нет побочных эффектов

1. Одинаковый вход => Одинаковый выход

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

Если вы сложите 2 и 4, всегда получите 6.

Ничего не влияет на результат.

Нечистые функции = непостоянные результаты

Второй пример ничего не возвращает. Он полагается на общее состояние для выполнения своей работы путем увеличения переменной за пределами своей области.

Эта модель кошмар для разработчиков.

Разделяемое состояние вводит зависимость от времени. Вы получаете разные результаты в зависимости от того, когда вы вызвали функцию. В первый раз результат 6, в следующий раз 10 и так далее.

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

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

Определенно в первом.

2. Нет побочных эффектов

image

Этот тест сам по себе контрольный список.

Примеры побочных эффектов:

  1. Видоизменение входных параметров
  2. console.log
  3. HTTP вызовы (AJAX/fetch)
  4. Изменение в файловой системе
  5. Запросы DOM

Советую посмотреть видео Боба Мартина.

Вот “нечистая” функция с побочным эффектом.

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

Однако, это может вызвать проблемы.

“Нечистое” изменение объекта

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

Разделяемое состояние означает, что влияние impureAssoc уже не полностью очевидно. Понимание влияния на систему теперь включает отслеживание каждой переменной, к которой когда-либо прикасалась, и знание ее истории.

Разделяемое состояние = временные зависимости.

Мы можем очистить impureAssoc, просто вернув новый объект с желаемыми свойствами.

“Очищаем это”

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

Можно было сделать и так:

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

Изменения ограничиваются этой небольшой областью, и вы все еще возвращаете значение.

Резюме

«Чистые» и «нечистые» функции в JavaScript

Что такое «чистые» и «нечистые» функции в JavaScript, и как их различать.

«Чистые» функции — это любые функции, исходные данные которых получены исключительно из их входных данных и не вызывают побочных эффектов в приложении. Математические функции являются примерами «чистых» функций. «Нечистые» функции бывают разных форм и размеров. Вот некоторые примеры:

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

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

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

Например у нас есть функция, которая получает одно значение x и возвращает в данном случае x + 1 :

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

Первая «нечистая» функция

Первая «нечистая» функция, которую мы собираемся сделать — это та, чей результат не основан исключительно на её входных данных. Например, давай рассмотрим функцию totalPrice . У нас есть глобальная переменная — COST_OF_ITEM , которая содержит цену на товар. Функция totalPrice берет quantity и умножает ее на эту переменную.

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

Хоть мы и получаем тот же результат, но это «нечистая» функция, так как состояние нашего приложения влияет на вывод нашей функции. Мы можем увидеть это, изменив значение COST_OF_ITEM и посмотреть снова в консоль.

Вторая «нечистая» функция

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

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

Давай используем нашу «нечистую» функцию generateID внутри фабричной функции для создания пользовательских объектов. Чтобы создать пользователя, createUser принимает два параметра: name и age , и возвращает объект с id , используя функцию generateID для его создания, а также name и age .

Вызовем createUser несколько раз, с одинаковыми аргументами.

Если посмотрим в консоль, то увидим, что мы получили похожие объекты, но они не одинаковые — id у всех разный.

«Нечистота» функции generateID делает нашу фабричную функцию createUser «нечистой». Для того чтобы исправить это — можно переместить «нечистую» функцию за пределы фабрики и вызвать её где-нибудь, где мы ожидаем побочный эффект, и передать id в качестве параметра в нашу фабрику createUser .

Третья «нечистая» функция

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

Допустим, мы отслеживаем изменяемое значение (в данном случае id ). Если мы создадим функцию, которая изменяет это значение, у нас будет «нечистая» функция. Например фабричная функция createPersone .

Если мы генерируем наш id для этого объекта, изменяя значение глобального id , то это «нечистая» функция. Вызвав эту функции несколько раз с разными name , то увидим, что id увеличился как мы и ожидали, но если мы также выведем в консоль глобальное значение id , то увидим, что оно тоже изменилось.

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

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

Время чтения: больше 15 мин

  1. Функция как элемент композиции
    1. Чистые функции и побочные эффекты
    2. Рекурсия
    3. Функции высших порядков
    4. Частичное применение
    5. Каррирование
    1. Функциональное ядро в императивной оболочке
    2. Контейнеры результатов
    3. Паттерн-матчинг
    1. Надёжность и удобство тестирования
    2. Оптимизация при компиляции
    3. Параллелизм и потокобезопасность
    1. Повышенное потребление памяти
    2. Сложность при работе с нечистыми сервисами
    1. Почему JavaScript функции являются объектами первого класса (First-class Objects)?
    2. Реализуйте функцию compose, которая в качестве аргументов принимает несколько функций одного аргумента и возвращает их композицию. Это тоже будет функция одного аргумента. compose(f, g, . )(x) == f(g(. (x))).

    Обновлено 5 января 2023

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

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

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

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

    Функция как элемент композиции

    Любая система состоит из частей. Программы — тоже системы со своими целями и средствами достижения этих целей. Сопоставление разных частей системы вместе называется композицией, а эти части — элементами композиции (composition units).

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

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

    • Границы должны быть проведены так, чтобы элемент решал только одну проблему, а не несколько (принцип разделения ответственности).
    • Элементы ничего не должны знать об устройстве других элементов, а общаться они должны через данные (закон Деметры).
    • Данные и настройки должны быть отделены от кода программы (The Twelve-Factor App).

    В функциональном программировании элемент композиции — это функция. Передача данных через несколько вызовов функций — их композиция. Например, если мы хотим к числу 10 прибавить 5, а потом умножить результат на 42, то последовательно вызовем функции add и multiply :

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

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

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

    Чистые функции и побочные эффекты

    Чтобы композиция функций была проще и не вызывала проблем, эти функции должны быть чистыми (pure). Чистая функция — это функция, которая не вызывает побочных эффектов (side effects), то есть никак не влияет на состояние внешнего мира.

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

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

    Например, pure Fn ( ) при вводе 10 и 20 всегда будет возвращать 15, значит она чистая:

    А impure Fn ( ) нечистая — она будет возвращать разные значения, потому что использует случайное число:

    И also Impure Fn ( ) тоже нечистая:

    В последних двух случаях функции производят побочные эффекты, потому что обращаются к глобальным объектам Math и Date . Да, любое взаимодействие с чем-либо «снаружи» функции считается побочным эффектом, даже получение значений.

    Дело в том, что мы не знаем, как именно устроены методы random ( ) и now ( ) в объектах снаружи. Они могут не только возвращать результат, но и менять состояние окружающего мира, например, меняя какую-то переменную.

    В примере ниже мы обращаемся к методу now ( ) , который всегда возвращает одно и то же значение, но попутно меняет значение переменной counter . Если мы не знаем, как устроен метод now ( ) , мы не можем гарантировать, что impure Fn ( ) не имеет побочных эффектов, поэтому считаем её тоже нечистой:

    Рекурсия

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

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

    Функции высших порядков

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

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

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

    Здесь новая функция filter ( ) , которая непосредственно перебирает значения. Она принимает на вход массив и функцию-предикат, которая проверяет каждое значение массива по своему условию.

    Реализуем filter ( ) самостоятельно, чтобы понять, как всё работает. Объявим функцию filter ( ) , в которую передадим два аргумента: массив и функцию, проверяющую условие — предикат.

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

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

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

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

    Вообще, в JavaScript filter ( ) уже есть, поэтому мы можем переписать код вот так:

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

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

    Хорошо, мы научились абстрагировать похожие задачи с разными аргументами. А что делать, если надо «запомнить» часть аргументов перед выполнением?

    Например, есть функция умножения multiply ( ) , но мы хотим дополнительно создать ещё и удвоитель double ( ) . Например, потому что он используется в программе чаще другого умножения.

    Решением в лоб было бы просто написать ещё одну функцию:

    Но мы видим, что схема выполнения обеих функций одинаковая. Просто в одном случае мы принимаем 2 аргумента, а в другом — 1, потому что второй аргумент «уже есть».

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

    Тогда создать удвоитель мы сможем, написав:

    Эта запись превратит double ( ) в функцию perform With ( ) , у которой аргумент a будет «заполнен заранее». То есть это:

    По сути равно этому:

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

    Таким же образом мы можем создать и утроитель и множитель на 10:

    Однако пользоваться самой функцией multiply ( ) становится непривычно, приходится вызывать функцию сразу после вызова функции:

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

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

    Каррирование – это трансформация функций таким образом, чтобы они принимали аргументы не как f ( a , b , c ) , а как f ( a ) ( b ) ( c ) . То есть это буквально то же, что мы сделали с функцией multiply ( ) , только автоматизировано.

    Попробуем сделать это в лоб:

    Вроде просто, но если аргументов будет больше 2, то придётся добавлять ещё одну обёртку. Поэтому лучше посчитать количество аргументов и автоматизировать создание обёрток:

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

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

    Также для частичного применения можно использовать `bind()`, хотя это и не очень «функционально».

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

    Работа с побочными эффектами

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

    А вот это уже труъ:

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

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

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

    • Запись данных в базу;
    • Получение данные от API;
    • Запрос к сети за картинкой.

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

    Функциональное ядро в императивной оболочке

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

    На примере обновления данных в базе это может выглядеть так:

    • сперва мы запрашиваем и получаем данные из базы, то есть производим побочный эффект;
    • затем преобразовываем данные с помощью чистой функции;
    • после записываем данные в базу, то есть снова производим побочный эффект.

    Получается такой сэндвич: побочный-эффект, чистое преобразование, побочный-эффект:

    Почипо-сэндвич: побочный эффект, чистое преобразование, побочный эффект

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

    В строгой парадигме всё несколько сложнее

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

    Мы не будем вдаваться в подробности этого подхода, потому что с наскока это будет сделать трудно. Основная его идея в том, что состояние — это не «что-то снаружи», а аргумент. Функция, которая принимает состояние и возвращает возможно изменённое состояние и будет функтором State.

    Контейнеры результатов

    Обычно в JavaScript ошибки обрабатывают императивно с помощью try — catch :

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

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

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

    Обе функции делают одно и то же, но во второй функции операция разбита на чёткие шаги. Сперва мы помещаем значение в массив — «контейнер». Затем мы используем map ( ) , чтобы преобразовать каждое значение из этого массива по некоторым правилам. В конце достаём из массива единственное значение, которое там было, но уже преобразованное.

    Такой поток выполнения линейный, в нём значение переходит от одного преобразования к следующему. Заметьте, что композиция этих преобразований у нас строится на поочерёдном вызове map ( ) на контейнере. Сейчас «контейнер» — это массив, но это совсем не обязательно.

    Мы можем реализовать собственный контейнер, операции с которым тоже можно будет компоновать с помощью map ( ) .

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

    Теперь мы можем соединять преобразования с помощью map ( ) — то есть использовать композицию:

    Чтобы достать значение из такого контейнера, код контейнера нужно слегка дополнить.

    Нам понадобится ещё один метод — fold ( ) , который сможет достать из замыкания функции Box ( ) нужное значение и вернуть его:

    Тогда достать значение с его помощью мы сможем так:

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

    Результат будет с типом Ok :

    Ошибка будет с типом Error :

    Обратите внимание, что Error при вызове map ( ) не выполняет переданную функцию. Это позволяет разветвлять код и обрабатывать разные случаи и ошибки, не заботясь о каждом этапе обработки ошибок отдельно.

    Теперь с помощью этих двух «коробок» мы можем решить, что именно хотим вернуть при работе с опасной операцией. Объявим функцию find Name ( ) , которая может вернуть undefined .

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

    С контейнером же мы можем не беспокоиться о случае с undefined :

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

    В примере выше если x — строка, к нему применится 3 преобразования из map и в конце на экране появится alert . В случае если x = = = undefined , то преобразования будут проигнорированы.

    Подробнее о том, что такое контейнеры, почему они играют такую важную роль, и как с их помощью обрабатывать исключения, посмотрите в курсе Professor Frisby Introduces Composable Functional JavaScript на Egghead.

    Паттерн-матчинг

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

    Концептуально он похож на switch:

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

    ƛ Если вам интересно узнать, как с помощью паттерн-матчинга написать, например, факториал, советуем заглянуть на learnyuohaskell.com.

    В JavaScript тоже можно (хоть и с костылями) использовать предикаты для паттерн-матчинга. Мы можем проверять результат выражений прямо в case :

    Но обычно, чтобы использовать паттерн-матчинг в JavaScript, подключают дополнительные библиотеки.

    Математические основы

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

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

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

    Если вам хочется по-настоящему узнать, что такое функторы, монады, семигруппы и прочее, то советуем прочитать спецификацию Fantasy Land. Будьте аккуратны, там сложно.

    Плюсы функционального программирования

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

    Надёжность и удобство тестирования

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

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

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

    Оптимизация при компиляции

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

    Параллелизм и потокобезопасность

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

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

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

    Повышенное потребление памяти

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

    Сложность при работе с нечистыми сервисами

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

    На собеседовании

    Это партнёрская рубрика, мы выпускаем её совместно с сервисом онлайн-образования Яндекс Практикум. Приносите вопрос, на который не знаете ответа, в задачи, мы разложим всё по полочкам и опубликуем. Если знаете ответ, присылайте пулреквест на GitHub.

    Почему JavaScript функции являются объектами первого класса (First-class Objects)?

    Читать:
    Как проверить обновление драйверов на виндовс 11

Похожие статьи