Хочешь уверенно проходить IT-интервью?

Мы понимаем, как сложно подготовиться: стресс, алгоритмы, вопросы, от которых голова идёт кругом. Но с AI тренажёром всё гораздо проще.
💡 Почему Т1 тренажёр — это мастхэв?
- Получишь настоящую обратную связь: где затык, что подтянуть и как стать лучше
- Научишься не только решать задачи, но и объяснять своё решение так, чтобы интервьюер сказал: "Вау!".
- Освоишь все этапы собеседования, от вопросов по алгоритмам до диалога о твоих целях.
Зачем листать миллион туториалов? Просто зайди в Т1 тренажёр, потренируйся и уверенно удиви интервьюеров. Мы не обещаем лёгкой прогулки, но обещаем, что будешь готов!
Реклама. ООО «Смарт Гико», ИНН 7743264341. Erid 2VtzqwP8vqy
Каррирование и функции высшего порядка – это основы функционального программирования. JavaScript поддерживает их "из коробки", а значит, мы просто обязаны использовать эти возможности для создания чистого высокопроизводительного кода, который легко переиспользовать.
Шаг 1. Функции первого класса
Одна из важных особенностей функционального программирования на JavaScript основана на том, что функции в языке являются "гражданами первого класса" (first-class citizens). Это означает, что вы можете делать с ними все, что придет в голову, например, передавать их как аргументы другим функциями или возвращать из других функций.
// простая JS-функция
const firstClassType = input => input;
// возвращаем функцию из другой функции
const myFunctionAsOutput = function () {
return firstClassType;
};
firstClassType('value'); // => 'value'
myFunctionAsOutput()('value'); // => 'value'
firstClassType
– это пример функции идентичности, которая принимает некоторое значение в качестве аргумента и его же возвращает, не изменяя.
myFunctionAsOutput
не принимает никаких параметров, а просто возвращает переменную firstClassType
, которая содержит функцию.
По сути вызов myFunctionAsOutput()
– это то же самое, что и прямое обращение к firstClassType
– эти выражения взаимозаменяемы. Поэтому мы можем вызвать результат работы myFunctionAsOutput()
как обычную функцию с помощью круглых скобок.
Следующий фрагмент делает то же самое, но более многословно.
const intermediateFn = myFunctionAsOutput();
intermediateFn('value');
Посмотрим на еще один пример, в котором функция выступает как параметр другой функции:
// принимаем функцию как аргумент в другую функцию и возвращаем ее
const myFunctionAsInputAndOutput = function(inputFn) {
return inputFn;
};
myFunctionAsInputAndOutput(firstClassType)('value'); // => 'value'
Функция myFunctionAsInputAndOutput
принимает функцию и ее же возвращает. По сути это просто еще один пример функции идентичности. То есть вызов myFunctionAsInputAndOutput(firstClassType)
– это то же самое, что и прямое обращение к firstClassType
, значит, мы тоже можем его вызвать.
То же самое можно сделать и с firstClassType
, если в качестве параметра input
передать функцию.
firstClassType(firstClassType)('value'); // => 'value'
Выражение firstClassType(firstClassType)
возвращает саму функцию firstClassType
, которую вновь можно вызвать :)
Единственная разница между двумя функциями идентичности firstClassType
и myFunctionAsInputAndOutput
состоит в том, что первая – это стрелочная функция, а вторая – обычная.
Шаг 2. Функции высшего порядка
Таким образом, все функции из предыдущего примера – это функции высшего порядка.
Вложенные функции (одна функция создается внутри другой) всегда имеют доступ к области видимости родительской функции, которая невидима снаружи. Таким образом, функция возвращенная из другой функции, может работать с данными, которые больше никому не видны. Это называется замыканием.
const memo = fn => {
// переменная memory находится в области видимости функции memo
// и не видна снаружи
let memory = [];
return anything => {
// область видимости вложенной функции
if(anything in memory) {
// вложенная функция читает переменную memory
return memory[anything];
} else {
const result = fn(anything);
// вложенная функция изменяет переменную memory
memory[anything] = result;
return result;
}
};
};
const upperCase = memo(a => a.toUpperCase());
upperCase('7urtle'); // => '7URTLE'
Это упрощенная реализация функции мемоизации. Давайте разбираться, что здесь происходит.
Например, у нас есть функция, которая приводит строку к верхнему регистру:
a => a.toUpperCase()
Мы хотим мемоизировать ее (предположим, что изменение регистра – это очень трудозатратная операция). Мы передаем эту функцию в функцию высшего порядка memo
и получаем взамен другую функцию, которую сохраняем в переменную upperCase
.
const upperCase = memo(a => a.toUpperCase());
Новая функция работает точно так же, как и старая: она принимает строку и возвращает ту же строку в верхнем регистре. Но под капотом используется техника мемоизации. Если входные данные повторяются, вычисление не происходит заново. Вместо этого берется ранее сохраненное значение из кеша memory
.
Шаг 3. Каррирование функций в JavaScript
Вам тоже кажется, что каррирование – это что-то из кулинарии? :)
На первом шаге мы уже составляли сложные выражения с несколькими круглыми скобками для последовательного вызова функций. Там одна функция возвращала другую, которую мы не записывали в переменную, а сразу же вызывали.
Каррирование выглядит точно так же:
const curry1 = function (a) {
return function (b) {
return a + b;
};
};
const curry2 = a => b => a + b;
curry2(5); // => b => 5 + b;
curry1(2)(3) === curry2(2)(3); // => 5
Функции curry1
и curry2
записаны по-разному, но работают абсолютно одинаково. Это сделано для наглядности, чтобы вы не запутались в стрелочных функциях.
Каждая стрелка в curry2
возвращает выражение, которое стоит справа от нее. Первая стрелка (первый вызов функции) возвращает вложенную функцию:
curry2(5); // b => 5 + b
При этом используется входящий аргумент первой функции – a
.
Второй вызов (вторые круглые скобки) вернет уже результат выполнения вложенной функции:
curry2(5)(10); // 15
Каррирование – это способ пошагового получения последовательности аргументов.
В curry2
всего два параметра (и две стрелки), но вы можете использовать сколько угодно при необходимости (только не забывайте о читаемости кода).
// один аргумент
const unary = a => a;
// два аргумента
// без каррирования
const binary = (a, b) => a + b;
// с каррированием
const curriedBinary = a => b => a + b;
// три аргумента
// без каррирования
const ternary = (a, b, c) => a + b + c;
// с каррированием
const curriedTernary = a => b => c => a + b + c;
ternary(1, 2, 3) === curriedTernary(1)(2)(3); // => 6
В функциональном программировании предпочтительно создавать унарные функции, которые имеют всего один аргумент. Из них уже составляются функции с большей арностью. Например, бинарная функция curriedBinary
составлена из двух унарных.
Шаг 4. Переиспользование каррированных функций
Каррированные функции очень удобны, так как могут быть легко переиспользованы в разных комбинациях для создания других функций.
const filter = checker => list => list.filter(checker);
const lowerCaseOf = input => input.toLowerCase();
const includes = what => where => where.includes(what);
const isTortoise = input => includes('tortoise')(lowerCaseOf(input));
const filterTortoises = filter(isTortoise);
const turtles = ['Greek Tortoise', 'Green Turtle'];
filterTortoises(turtles); // => ['Greek Tortoise']
В этом примере мы используем три унарные функции filter
, lowerCaseOf
и includes
. Из них мы собираем сложные функции isTortoise
и filterTortoises
, как конструктор Lego из отдельных деталек.
Шаг 5. Улучшенная композиция функций
Каррировать JavaScript-функции при каждом использовании – занятие раздражающее, поэтому лучше использовать уже готовую библиотеку со множеством утилит, например, @7urtle/lambda. Помимо прочего она предоставляет полезную функцию compose
для удобной композиции нескольких функций.
// без compose
input => includes('tortoise')(lowerCaseOf(input))
// с compose
compose(includes('tortoise'), lowerCaseOf)
Вы передаете ей несколько функций в качестве аргументов, через запятую, а она выстраивает из них конвейер. При этом выполнение функций будет осуществляться справа налево, то есть функция, которую вы передадите последней, выполнится первой, а ее результат будет использован предпоследней функцией.
import {lowerCaseOf, includes, filter, compose, memo} from '@7urtle/lambda';
// мемоизированный конвейер из функций includes и lowerCaseOf
const isTortoise = memo(compose(includes('tortoise'), lowerCaseOf));
const filterTortoises = filter(isTortoise);
const turtles = ['Greek Tortoise', 'Green Turtle'];
filterTortoises(turtles); // => ['Greek Tortoise']
Обратите внимание, как изящно и коротко (всего в одной строчке кода) нам удалось определить функцию isTortoise
, которая на самом деле выполняет большой объем работы. И все это практически без потери читаемости.
Альтернативный способ сделать то же самое – функция imperativeFilterTortoises
.
const imperativeFilterTortoises = function(turtles) {
let memory = [];
let tortoises = [];
for (let i = 0; i < turtles.length; i++) {
let isTortoise;
if(turtles[i] in memory) {
isTortoise = memory[turtles[i]];
} else {
isTortoise = turtles[i].toLowerCase().includes('tortoise');
memory[turtles[i]] = isTortoise;
}
if(isTortoise) {
tortoises.push(turtles[i]);
}
}
return tortoises;
};
imperativeFilterTortoises(turtles); // => ['Greek Tortoise']
Она написана в императивном стиле и занимает целых 17 строк (515 символов против 115 у декларативного варианта)!
Бонус: функции curry и nary
Вероятно, вы уже задумались о поиске или создании волшебной функции, которая автоматически каррирует вашу n-арную функцию, или позволит вызывать каррированную функцию в привычном стиле с несколькими аргументами.
Тогда вам нужны функции curry
и nary
. Первая принимает n-арную функцию, вторая – каррированную. Обе позволяют вызывать полученные функции в любом стиле (как каррированные или как n-арные).
const curry = fn =>
(...args) => args.length >= fn.length
? fn(...args)
: (...args2) => curry(fn)(...args, ...args2);
const nary = fn =>
(...args) => args.length === 0
? fn()
: args.reduce(
(accumulator, current) => accumulator(current),
fn
);
const fromBinary = curry((a, b) => a + b);
const fromCurried = nary(a => b => a + b);
fromBinary(2, 3) === fromBinary(2)(3); // => true
fromCurried(2, 3) === fromCurried(2)(3); // => true
fromBinary(2, 3) === fromCurried(2, 3); // => true
Обратите внимание, что сами функции curry
и nary
написаны в декларативном функциональном стиле. Они обе доступны в библиотеке @7urtle/lambda. Фактически, библиотека под капотом использует функцию nary
, чтобы обеспечить возможность вызывать пользовательские функции любым удобным способом.
Познав магию функционального программирования на JavaScript, вы не захотите возвращаться обратно. Это очень простой и элегантный способ создания кода, который нельзя не оценить.
Комментарии