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

Возврат функций из функций JS: Функциональное программирование

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

Для закрепления материала пройдитесь по нему два раза. Первый раз просто бегло прочитайте, второй раз изучите внимательно, проверяя каждую строчку кода на сервисе repl.it.

Начнем погружение с уже пройденного материала:

const identity = (v) => v;
identity('wow'); // wow

const sum = identity((a, b) => a + b);
sum(1, 8); // 9

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

const generateSumFinder = () => {
  const sum = (a, b) => a + b;     // создали функцию
  return sum;                      // и вернули ее
};

const sum = generateSumFinder();   // sum теперь — функция, которую вернула функция generateSumFinder
sum(1, 5); // 6                 // sum складывает числа

Можно даже обойтись без промежуточного создания константы:

// вызвали функцию, которая возвращает функцию,
// и тут же вызвали возвращенную функцию

generateSumFinder()(1, 5); // 6
// ((a, b) => a + b)(1, 5)

Всегда, когда видите подобные вызовы f()()(), знайте: функции возвращаются!

Теперь посмотрим, как еще можно описать функцию generateSumFinder:

// предыдущий вариант для сравнения
// const generateSumFinder = () => {
//   const sum = (a, b) => a + b;
//   return sum;
// };

// новый вариант
const generateSumFinder = () => (a, b) => a + b;

Для понятности можно расставить скобки:

const generateSumFinder = () => ((a, b) => a + b);

Определение функции обладает правой ассоциативностью. Все, что находится справа от =>, считается телом функции. Количество вложений никак не ограничено. Вполне можно встретить и такие варианты:

const sum = (x) => (y) => (z) => x + y + z;

// расставим скобки для того чтобы увидеть как функции вложены друг в друга
// const sum = x => (y => (z => x + y + z));

sum(1)(3)(5); // 9

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

const inner1 = (z) => x + y + z;
const inner2 = (y) => inner1;
const sum = (x) => inner2;

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

sum(1)(3)(5); // 9

const sum1 = (x) => (y) => (z) => x + y + z;

// sum(1);
const sum2 = (y) => (z) => 1 + y + z; // inner2

// sum(1)(3)
const sum3 = (z) => 1 + 3 + z; // inner1

// sum(1)(3)(5)
const sum4 = 1 + 3 + 5; // 9

Как видно выше, sum1, sum2 и sum3 — это функции, а sum4 уже число, так как были вызваны все внутренние функции.

Давайте распишем все функции:

const sum = (x) => (y) => (z) => x + y + z;
// const sum = x => (y => (z => x + y + z));
  • Функция sum принимает x и возвращает функцию, которая
    • принимает y и возвращает функцию, которая
      • принимает z и возвращает сумму x + y + z

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

const generate = (f) => (arg) => f(f(arg));
// const generate = f => (arg => f(f(arg)));

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

Closure

Создадим функцию f1. Она будет той функцией, которую вернет generate если передать ей функцию Math.sqrt (она вычисляет квадратный корень числа).

Получается, f1 — это функция, которая принимает число и возвращает корень корня — Math.sqrt(Math.sqrt(x)):

const f1 = generate(Math.sqrt);
f1(16); // 2
// generate(Math.sqrt)(16);

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

const f2 = generate(x => x ** 2);
f2(4); // 256
// generate(x => x ** 2)(4);

Теперь функция f2 возводит число в квадрат два раза: (42)2.

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

const f1 = generate(x => x ** 2);
const f2 = generate(x => x ** 2);
console.log(f1 === f2); // => false

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

Замыкание

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

const generateDouble = (f) => (arg) => f(f(arg));
const f1 = generateDouble(Math.sqrt);

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

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

Замыкание

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

В СИКП дается прекрасный пример на понимание замыканий. Представьте себе, что мы проектируем систему, в которой нужно запомнить пароль пользователя, а потом проверять его, когда пользователь будет заново заходить. Можно смоделировать функцию savePassword, которая принимает на вход пароль и возвращает предикат, то есть функцию, возвращающую true или false, для его проверки. Посмотрите, как это выглядит:

const secret = 'qwerty';
// Возвращается предикат.
const isCorrectPassword = savePassword(secret);

// Теперь можно проверять
console.log(isCorrectPassword('wrong password')); // => false
console.log(isCorrectPassword('qwerty')); // => true

А вот как выглядит код функции savePassword:

const savePassword = password => passwordForCheck => password === passwordForCheck;

Возврат функций в реальном мире (Debug)

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

[  DEBUG] [2015-11-19 19:02:30.836222] accept: HTTP/1.1 GET - / - 200, 4238
[   INFO] [2015-11-19 19:02:32.106331] config: server has reload its config in 200 ms
[WARNING] [2015-11-19 19:03:12.176262] accept: HTTP/1.1 GET - /info - 404, 829
[  ERROR] [2015-11-19 19:03:12.002127] accept: HTTP/1.1 GET - /info - 503, 829

В JavaScript самой популярной библиотекой для логгирования считается Debug. Вот как выглядит ее вывод:

Debug

Обратите внимание на левую часть каждой строки. Debug для каждой выводимой строчки использует так называемый неймспейс, некоторую строчку, которая указывает принадлежность выводимой строчки к определенной подсистеме или части кода. Он используется для фильтрации, когда логов становится много. Другими словами, можно указать "выводи сообщения только для http". А вот как это работает:

import debug from 'debug';

const logHttp = debug('http');
const logHandler = debug('handler');

logHttp('hello!');
logHttp('i am from http');

logHandler('hello from handler!');
logHandler('i am from handler');

Что приведет к такому выводу:

http hello! +0ms
http i am from http +2ms
handler hello from handler! +0ms
handler i am from handler +1ms

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


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

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

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»