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

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

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

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

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

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