Жаргон функционального программирования
- пятница, 16 октября 2026 г. в 00:00:07
Функциональное программирование (ФП) предоставляет много преимуществ, и, как следствие, его популярность растет. Однако, каждая парадигма программирования имеет собственный уникальный жаргон и ФП не исключение. Цель этого словаря терминов — облегчить изучение ФП.
Примеры написаны на JavaScript (ES2015). Почему JavaScript?
Где уместно, в этом документе используются термины, определенные в Fantasy Land spec.
Содержание
Арность
Количество аргументов, принимаемых функцией. От слов, вроде “унарный”, “бинарный”, “тернарный” и т.п.
const sum = (a, b) => a + b // Арность sum - 2 (бинарный) const inc = a => a + 1 // Арность inc - 1 (унарный) const zero = () => 0 // Арность zero - 0 (нульарный)
Дополнительные материалы
Арность в Википедии
Функция более высокого порядка
Функция, принимающая другую функцию в качестве аргумента и/или возвращающая функцию:
const filter = (predicate, xs) => xs.filter(predicate)
const is = (type) => (x) => Object(x) instanceof type
filter(is(Number), [0, '1', 2, null]) // [0, 2]
Замыкание
Замыкание — это область, которая захватывает локальные переменные функции и делает их доступными даже после того, как выполнение кода выходит из блока, в котором они определены. Это позволяет значениям в замыкании быть доступными возвращающим функциям.
const addTo = x => y => x + y const addToFive = addTo(5) addToFive(3) // => 8
В данном случае x сохраняется в замыкании addToFive со значением 5. addToFive затем может вызываться с y для получения суммы.
Дополнительные материалы
Частичное применение
Частичное применение функции означает создание новой функции путем предварительного определения некоторых аргументов исходной функции:
// Утилита для создания частично примененных функций. // Принимает функцию и аргументы const partial = (f, ...args) => // Возвращает функцию, которая принимает другие аргументы (...moreArgs) => // и вызывает оригинальную функцию со всеми аргументами f(...args, ...moreArgs) // Нечто для применения const add3 = (a, b, c) => a + b + c // Частичное применение `2` и `3` к `add3` дает функцию с одним параметром const fivePlus = partial(add3, 2, 3) // (c) => 2 + 3 + c fivePlus(4) // 9
Для частичного применения функций в JS также можно использовать Function.prototype.bind:
const add1More = add3.bind(null, 2, 3) // (c) => 2 + 3 + c
Частичное применение помогает создавать более простые функции из более сложных путем предварительного сохранения данных при их наличии.
Каррирование
Процесс преобразования функции, принимающей несколько аргументов в функцию, принимающую один аргумент за раз.
При каждом вызове функция принимает только один аргумент и возвращает функцию, которая также принимает один аргумент, до тех пор, пока не будут переданы все аргументы:
const sum = (a, b) => a + b const curriedSum = (a) => (b) => a + b curriedSum(40)(2) // 42 const add2 = curriedSum(2) // (b) => 2 + b add2(10) // 12
Автокаррирование
Преобразование функции, которая принимает несколько аргументов в функцию, которая при передаче меньшего количества аргументов, чем требуется, возвращает функцию, принимающую оставшиеся аргументы. Функция вычисляется только после передачи всех аргументов.
Lodash и Ramda предоставляют функцию curry, которая работает именно так.
const add = (x, y) => x + y const curriedAdd = _.curry(add) curriedAdd(1, 2) // 3 curriedAdd(1) // (y) => 1 + y curriedAdd(1)(2) // 3
Дополнительные материалы
Композиция функций
Объединение двух функций в одну, где результат одной функции передается на вход другой функции. Эта одна из наиболее важных концепций ФП.
const compose = (f, g) => (a) => f(g(a)) // Определение const floorAndToString = compose((val) => val.toString(), Math.floor) // Использование floorAndToString(121.212121) // '121'
Продолжение
В любой момент выполнения программы часть кода, которая сейчас будет выполняться, называется продолжением.
const printAsString = (num) => console.log(`Given ${num}`) const addOneAndContinue = (num, cc) => { const result = num + 1 cc(result) } addOneAndContinue(2, printAsString) // 'Given 3'
Продолжения часто встречаются в асинхронном программировании, когда программе нужно ждать данных, чтобы продолжить выполнение. После получения данных они часто передаются другой части программы, которая является продолжением:
const continueProgramWith = (data) => { // Продолжение выполнения программы с данными } readFileAsync('path/to/file', (err, response) => { if (err) { // Обработка ошибки return } // Передача ответа continueProgramWith(response) })
Чистая структура данных, которая инкапсулирует сторонний эффект. Вместо выполнения эффекта сразу, IO оборачивает операцию в нульарную (nullable) функцию (thunk), позволяя преобразовывать операции с эффектами, создавать из них цепочки и композиции чистых значений без их выполнения до явного вызова.
const IO = (run) => ({ run, map: (f) => IO(() => f(run())), chain: (f) => IO(() => f(run()).run()) }) // Чистое описание — пока ничего не выполняется const readTimestamp = IO(() => Date.now()) const formatted = readTimestamp.map((ts) => new Date(ts).toISOString()) // Сторонний эффект выполняется только после вызова .run() formatted.run()
Дополнительные материалы
Трамплин/батут
Механизм, позволяющий глубоким или взаимно рекурсивным функциям выполняться без переполнения стека вызовов.
В средах выполнения без Tail Call Optimization (TCO) (оптимизации хвостовых вызовов), рекурсивные вызовы возвращают функцию (thunk) вместо прямого выполнения. Трамплин запускает цикл while, который разматывает каждый thunk до достижения финального значения.
const trampoline = (fn) => (...args) => { let result = fn(...args) while (typeof result === 'function') { result = result() } return result } // Без трамплина: sumBelow(1000000) выбрасывает "Maximum call stack size exceeded" const sumBelow = (n, acc = 0) => n === 0 ? acc : () => sumBelow(n - 1, acc + n) // возвращает thunk вместо прямого выполнения const safeSum = trampoline(sumBelow) safeSum(1000000) // 500000500000
Дополнительные материалы
Нульарная (nullable) функция (функция, не принимающая аргументы), которая оборачивает выражение для задержки его вычисления до вызова. Thunk — фундаментальный механизм реализации ленивых вычислений, трамплинов и отложенных сторонних эффектов.
// Жадное (eager) вычисление выполняется сразу: // const data = expensiveCalculation() // Thunk оборачивает выражение в функцию, откладывая ее выполнение: const thunk = () => 42 * 2 // Выражение вычисляется только при явном вызове: thunk() // 84
Дополнительные материалы
Thunk on Wikipedia
Алгебраические эффекты
Система вычислительных эффектов, которая отделяет вызов эффекта от его обработки. Вместо привязки функции к среде выполнения, функция “выполняет” операцию эффекта (такую как чтение состояния, запрос конфигурации или логгирование). Внешний “обработчик” перехватывает выполняемый эффект и предоставляет результат, позволяя возобновить вычисления или прервать их; это обобщает механизмы исключений, async/await и генераторов, не требуя использования сложных стеков трансформеров монад.
// Генераторы моделируют ограниченные продолжения / алгебраические эффекты: const perform = (effect) => ({ [Symbol.for('effect')]: true, effect }) // Программа выполняет эффекты без знания того, кто их обрабатывает: function * fetchUserProfile (userId) { const config = yield perform({ type: 'ask_config' }) yield perform({ type: 'log', message: `Fetching user ${userId} from ${config.apiUrl}` }) return { id: userId, name: 'Alice' } } // Обработчик эффектов перехватывает их и продолжает вычисление: const handle = (generator, handlers) => { const iter = generator() const step = (value) => { const { done, value: yielded } = iter.next(value) if (done) return yielded if (yielded && yielded[Symbol.for('effect')]) { const { type } = yielded.effect if (handlers[type]) { return handlers[type](yielded.effect, (resumeVal) => step(resumeVal)) } } return step(yielded) } return step() } // Запуск с интерпретатором / обработчиком: handle( () => fetchUserProfile(42), { ask_config: (effect, resume) => resume({ apiUrl: 'https://api.test.local' }), log: (effect, resume) => { console.log(effect.message) return resume() } } )
Дополнительные материалы
Чистая функция
Функция называется чистой, если возвращаемое ей значение определяется только входящими значениями, и она не выполняет сторонние эффекты. Функция должна возвращать одинаковый результат для одинаковых входных данных.
const greet = (name) => `Hi, ${name}` greet('Brianne') // 'Hi, Brianne'
Примеры “нечистых” функций:
window.name = 'Brianne' const greet = () => `Hi, ${window.name}` greet() // "Hi, Brianne"
Здесь результат зависит от данных, хранящихся за пределами функции.
let greeting const greet = (name) => { greeting = `Hi, ${name}` } greet('Brianne') greeting // "Hi, Brianne"
Здесь модифицируется состояние, не принадлежащее функции.
Сторонние эффекты
Функция или выражение содержат сторонний эффект, если, помимо возврата значения, они взаимодействуют с (читаю из или пишут в) внешнее модифицируемое состояние:
const differentEveryTime = new Date()
console.log('IO is a side effect!')
Идемпотентность
Функция является идемпонтентной, если ее повторное применение всегда дает одинаковый результат:
Math.abs(Math.abs(10))
sort(sort(sort([2, 1])))
Беспараметрический стиль
Написание функций, где дефиниция не определяет используемые аргументы явно. Этот стиль обычно требует каррирования или других функций более высокого порядка. Другое название — бесточечное программирование (tacit programming).
// Дано const map = (fn) => (list) => list.map(fn) const add = (a) => (b) => a + b // Затем // Не беспараметрический стиль - `numbers` - явный аргумент const incrementAll = (numbers) => map(add(1))(numbers) // Беспараметрический стиль - `list` - неявный аргумент const incrementAll2 = map(add(1))
Такие определения функций выглядят как обычные присваивания без function или =>. Следует отметить, что беспараметрические функции не обязательно лучше обычных, поскольку они могут быть более сложными в понимании в сложных случаях.
Предикат
Предикат — это функция, возвращающая true или false для переданного значения. Частый случай использования — коллбек для фильтрации массива.
const predicate = (a) => a > 2 ;[1, 2, 3, 4].filter(predicate) // [3, 4]
Контракт
Контракт определяет обязательства и гарантии относительно поведения функции или выражения во время выполнения. Он представляет собой набор правил, касающихся входных и выходных данных функции или выражения, и, как правило, при нарушении контракта возникает ошибка:
// Определяем наш контракт: int -> boolean const contract = (input) => { if (typeof input === 'number') return true throw new Error('Contract violated: expected int -> boolean') } const addOne = (num) => contract(num) && num + 1 addOne(2) // 3 addOne('some string') // контракт нарушен: ожидалось int -> boolean
Категория
В теории категорий категория представляет собой совокупность объектов и морфизмов (morphisms) между ними. В программировании объектами обычно выступают типы, а морфизмами — функции.
Три правила валидной категории:
Должен существовать морфизм тождества, отображающий объект в самого себя. Если a — объект некоторой категории, то должно существовать отображение a -> a.
Морфизмы должны обладать свойством композиции. Если a, b и c — объекты некоторой категории, f — морфизм из a в b, а g — морфизм из b в c, то g(f(x)) должно быть эквивалентно (g • f)(x).
Композиция должна быть ассоциативной: f • (g • h) — это то же самое, что (f • g) • h.
Поскольку эти правила регулируют композицию на очень высоком уровне абстракции, теория категорий прекрасно подходит для выявления новых способов компоновки объектов.
В качестве примера мы можем определить категорию Max как класс:
class Max { constructor (a) { this.a = a } id () { return this } compose (b) { return this.a > b.a ? this : b } toString () { return `Max(${this.a})` } } new Max(2).compose(new Max(3)).compose(new Max(5)).id().id() // => Max(5)
Дополнительные материалы
Полугруппоид
Алгебраическая структура с объектами и морфизмами, которые можно ассоциативно компоновать, но которая не гарантирует существования тождественного морфизма для каждого объекта.
Полугруппоид удовлетворяет свойству ассоциативности для композиции: f.compose(g).compose(h) === f.compose(g.compose(h)).
Любая категория является полугруппоидом, однако для полугруппоида наличие морфизма тождества (id) не требуется. Композиция функций образует естественный пример полугруппоида:
const Semigroupoid = (fn) => ({ run: fn, compose: (other) => Semigroupoid((x) => fn(other.run(x))) }) const toUpper = Semigroupoid((s) => s.toUpperCase()) const exclaim = Semigroupoid((s) => `${s}!`) const loudGreeting = exclaim.compose(toUpper) loudGreeting.run('hello') // 'HELLO!'
Дополнительные материалы
Semigroupoid in Fantasy Land
Значение
Все, что может быть присвоено переменной:
5 Object.freeze({ name: 'John', age: 30 }) // функция `freeze` обеспечивает иммутабельность ;(a) => a ;[1] undefined
Константа
Переменная, значение которой не может меняться после определения:
const five = 5 const john = Object.freeze({ name: 'John', age: 30 })
Константы ссылочно прозрачны. Поэтому они могут быть заменены значениями, которые они представляют, без влияния на результат.
Следующее выражение всегда возвращает true:
john.age + five === ({ name: 'John', age: 30 }).age + 5
Константная функция
Каррированая функция, игнорирующая второй аргумент:
const constant = a => () => a ;[1, 2].map(constant(0)) // => [0, 0]
Константный функтор
Объект, чей метод map не модифицирует его содержимое. См. функтор.
Constant(1).map(n => n + 1) // => Constant(1)
Константная монада
Объект, чем метод chain не модифицирует его содержимое. См. монада.
Constant(1).chain(n => Constant(n + 1)) // => Constant(1)
Функтор
Объект, реализующий функцию map, которая принимает функцию, которая выполняется на содержимом этого объекта. Функтор должен удовлетворять двум условиям:
Сохранение идентичности
object.map(x => x)
это эквивалент простого object.
Компонуемость
object.map(x => g(f(x)))
это эквивалент следующего:
object.map(f).map(g)
(f, g - произвольные компонуемые функции).
Эталонная реализация опции является функтором, поскольку удовлетворяет следующим условиям:
Some(1).map(x => x) // = Some(1)
и
const f = x => x + 1 const g = x => x * 2 Some(1).map(x => g(f(x))) // = Some(4) Some(1).map(f).map(g) // = Some(4)
Пунктированный функтор
Это объект с функцией of, которая добавляет в него любое единичное значение.
ES2015 добавил метод Array.of, сделав массив пунктированным функтором:
Array.of(1) // [1]
Подъем/поднятие
Поднятие — это когда мы берем значение и помещаем его в объект, вроде функтора. Если функция поднимается в аппликативный функтор, можно сделать так, чтобы она работала со значениями, которые также находятся в функторе.
Некоторые реализации имеют функцию под названием lift или liftA2 для облегчения запуска функций на функторах:
const liftA2 = (f) => (a, b) => a.map(f).ap(b) // заметьте, что второй метод — `ap`, а не `map` const mult = a => b => a * b const liftedMult = liftA2(mult) // эта функция не работает на функторах, как массив liftedMult([1, 2], [3]) // [3, 6] liftA2(a => b => a + b)([1, 2], [30, 40]) // [31, 41, 32, 42]
Поднятие одноаргументной функции и ее применение аналогично тому, что делает map:
const increment = (x) => x + 1 lift(increment)([2]) // [3] ;[2].map(increment) // [3]
Поднятие простых значений может быть просто созданием объекта:
Array.of(1) // => [1]
Ссылочная прозрачность
Выражение, которое можно заменить его значением без изменения поведения программы, называется ссылочно (референциально) прозрачным.
Рассмотрим такую функцию:
const greet = () => 'Hello World!'
Любой вызов greet() можно заменить на Hello World!, следовательно, функция greet обладает свойством референциальной прозрачности. Это свойство было бы нарушено, если бы greet зависела от внешнего состояния, например, от конфигурации или обращения к базе данных. См. также чистые функции и рассуждение на основе равенств.
Рассуждение на основе равенств
Когда приложение состоит из выражений и не имеет побочных эффектов, выводы о системе можно сделать на основе анализа ее отдельных частей. Кроме того, можно быть уверенным в деталях работы системы, не изучая при этом каждую функцию.
const grainToDogs = compose(chickenIntoDogs, grainIntoChicken) const grainToCats = compose(dogsIntoCats, grainToDogs)
В приведенном примере, если мы знаем, что chickenIntoDogs и grainIntoChicken — чистые функции, тогда мы знаем, что композиция чистая. Этот подход можно развить дальше, когда станет больше известно о свойствах функций (ассоциативности, коммутативности, идемпотентности и т.д.).
Мемоизация
Техника оптимизации, которая кэширует возвращаемое функцией значение на основе ее входных параметров. Мемоизация валидна и безопасна только для чистых функций, обладающих ссылочной прозрачностью, поскольку вызов функции с идентичными аргументами должен всегда возвращать одинаковые результаты без наблюдаемых сторонних эффектов.
const memoize = (fn) => { const cache = new Map() return (arg) => { if (!cache.has(arg)) { cache.set(arg, fn(arg)) } return cache.get(arg) } } const factorial = memoize((n) => (n <= 1 ? 1 : n * factorial(n - 1))) factorial(5) // вычислено: 120 factorial(5) // извлечено из кэша: 120
Дополнительные материалы
Мемоизация в Википедии
Лямбда
Анонимная функция, с которой можно обращаться как со значением:
;(function (a) { return a + 1 }) ;(a) => a + 1
Лямбды часто передаются как аргументы в функции более высокого порядка:
;[1, 2].map((a) => a + 1) // [2, 3]
Лямбду можно присвоить переменной:
const add1 = (a) => a + 1
Лямбда-исчисление
Раздел математики, использующий функции для создания универсальной модели вычислений.
Функциональный комбинатор
Функция более высокого порядка, как правило, каррированная, которая возвращает новую функцию, модифицированную тем или иным образом. Функциональные комбинаторы часто используются в беспараметрическом стиле для написания особо лаконичных программ.
// Комбинатор "C" принимает каррированную двуаргументную функцию и возвращает функцию, // которая вызывает оригинальную функцию с аргументами в обратном порядке const C = (f) => (a) => (b) => f(b)(a) const divide = (a) => (b) => a / b const divideBy = C(divide) const divBy10 = divideBy(10) divBy10(30) // => 3
См. также список функциональных комбинаторов в JavaScript.
Ленивое вычисление
Ленивые вычисления — это механизм вычисления по требованию, при котором вычисление выражения откладывается до момента, когда его значение действительно необходимо. В функциональных языках это позволяет создавать такие структуры, как бесконечные списки, которые обычно недоступны в императивных языках, где важна последовательность выполнения команд.
const rand = function * () { while (1 < 2) { yield Math.random() } }
const randIter = rand() randIter.next() // каждое выполнение дает произвольное значение, выражение оценивается по требованию
Моноид
Объект с функцией, которая “комбинирует” этот объект с другим объектом того же типа (полугруппа), который имеет “идентичное” значение.
Примером простого моноида является сложение чисел:
1 + 1 // 2
В данном случае число — это объект, а + — функция.
Когда любое значение комбинируется с идентичным значением, результатом должно быть оригинальное значение. Идентичность также должна быть коммутативной.
Идентичным значением для сложения является 0:
1 + 0 // 1 0 + 1 // 1 1 + 0 === 0 + 1
Также требуется, чтобы группировка операций не влияла на результат (ассоциативность):
1 + (2 + 3) === (1 + 2) + 3 // true
Конкатенация массивов также формирует моноид:
;[1, 2].concat([3, 4]) // [1, 2, 3, 4]
Идентичным значением в данном случае является пустой массив []:
;[1, 2].concat([]) // [1, 2]
Напротив, вычитание не формирует моноид, поскольку не существует для него коммутативного идентичного значения:
0 - 4 === 4 - 0 // false
Монада
Монада — это объект с функциями of и chain. chain похожа на map, за исключением того, что она раскрывает (unnest) вложенные объекты.
// Реализация Array.prototype.chain = function (f) { return this.reduce((acc, it) => acc.concat(f(it)), []) } // Использование Array.of('cat,dog', 'fish,bird').chain((a) => a.split(',')) // ['cat', 'dog', 'fish', 'bird'] // Пример с map Array.of('cat,dog', 'fish,bird').map((a) => a.split(',')) // [['cat', 'dog'], ['fish', 'bird']]
of также именуется return в других функциональных языках, а chain - flatmap и bind.
Комонада
Объект, содержащий функции extract и extend:
const CoIdentity = (v) => ({ val: v, extract () { return this.val }, extend (f) { return CoIdentity(f(this)) } })
extract извлекает значение из функтора:
CoIdentity(1).extract() // 1
extend запускает функцию на комонаде. Функция должна возвращать тот же тип, что и комонада:
CoIdentity(1).extend((co) => co.extract() + 1) // CoIdentity(2)
Композиция Клейсли
Операция композиции двух функций, возвращающих монаду (стрелки Клейсли), при условии совместимости их типов. В Haskell это оператор >=>.
С помощью опции:
// safeParseNum :: String -> Number Option const safeParseNum = (b) => { const n = parseNumber(b) return isNaN(n) ? None() : Some(n) } // validatePositive :: Number -> Number Option const validatePositive = (a) => a > 0 ? Some(a) : None() // kleisliCompose :: Monad M => ((b -> M c), (a -> M b)) -> a -> M c const kleisliCompose = (g, f) => (x) => f(x).chain(g) // parseAndValidate :: String -> Number Option const parseAndValidate = kleisliCompose(validatePositive, safeParseNum) parseAndValidate('1') // => Some(1) parseAndValidate('asdf') // => None parseAndValidate('999') // => Some(999)
Это работает, поскольку:
и validatePositive, и safeParseNum возвращают одинаковый тип монад (Option),
тип аргумента validatePositive совпадает с раскрытым (unwrapped) результатом safeParseNum.
Свободная монада
Свободная монада — это конструкция, позволяющая создать монаду из любого функтора без добавления какой-либо предметно-специфичной логики. Она обеспечивает четкое разделение описания программы (абстрактного синтаксического дерева команд) и ее выполнения (интерпретатора, вычисляющего это дерево).
Свободная монада бывает двух видов:
Pure: оборачивает финальное значение и прерывает вычисление.
Free: оборачивает функтор, содержащий следующий шаг вычисления.
// Конструкторы свободных монад: const Pure = (x) => ({ isPure: true, value: x, map: (f) => Pure(f(x)), chain: (f) => f(x) }) const Free = (fn) => ({ isPure: false, functor: fn, map: (f) => Free(fn.map((next) => next.map(f))), chain: (f) => Free(fn.map((next) => next.chain(f))) }) // Поднятие инструкции функтора в свободную монаду: const liftF = (cmd) => Free(cmd.map(Pure)) // Функтор, представляющий инструкции логгирования: const Log = (msg, next) => ({ type: 'log', msg, next, map: (f) => Log(msg, f(next)) }) // Программа: описание операций без их выполнения const logMsg = (msg) => liftF(Log(msg, null)) const program = logMsg('Starting').chain(() => logMsg('Done')).chain(() => Pure(42)) // Интерпретатор: выполняет дерево инструкций const interpret = (freeMonad) => { if (freeMonad.isPure) return freeMonad.value const { type, msg, next } = freeMonad.functor if (type === 'log') { console.log(msg) return interpret(next) } } interpret(program) // логгирует 'Starting', 'Done', возвращает 42
Дополнительные материалы
Трансформер монад
В то время как функторы и аппликативные функторы естественным образом компонуются друг с другом, монады в общем случае не поддаются композиции без знания их конкретных типов. Трансформер монад — это конструктор типов, который принимает существующую монаду и создает новую, обладающую объединенными возможностями (например, сочетающую в себе обработку ошибок, асинхронные задачи и работу с состоянием).
Названия трансформеров монад, обычно, заканчивается на T (например, MaybeT, ReaderT, StateT).
// MaybeT оборачивает любую внешнюю монаду M для добавления опциональности: const MaybeT = (M) => { const of = (value) => MaybeTInstance(M.of({ isSome: true, value })) const none = () => MaybeTInstance(M.of({ isSome: false })) const MaybeTInstance = (run) => ({ run, chain: (f) => MaybeTInstance( run.chain((opt) => (opt.isSome ? f(opt.value).run : M.of(opt))) ), map: (f) => MaybeTInstance( run.map((opt) => (opt.isSome ? { isSome: true, value: f(opt.value) } : opt)) ) }) return { of, none, from: MaybeTInstance } } // Монада тождества: const Id = (x) => ({ value: x, map: (f) => Id(f(x)), chain: (f) => f(x) }) Id.of = Id // Объединение монады с переданным Id с эффектом Maybe: const MaybeId = MaybeT(Id) const findUser = (id) => id === 1 ? MaybeId.of({ name: 'Alice', age: 30 }) : MaybeId.none() const getAge = (id) => findUser(id) .chain((user) => MaybeId.of(user.age)) .run getAge(1).value // { isSome: true, value: 30 } getAge(2).value // { isSome: false }
Дополнительные материалы
Аппликативный функтор
Аппликативный функтор — это объект с функцией ap. ap применяет функцию в объекте к значению другого объекта с тем же типом.
// Реализация Array.prototype.ap = function (xs) { return this.reduce((acc, f) => acc.concat(xs.map(f)), []) } // Пример использования ;[(a) => a + 1].ap([1]) // [2]
Это полезно, когда есть два объекта и нужно применить бинарную функцию к их содержимому:
// Массивы, которые нужно объединить const arg1 = [1, 3] const arg2 = [4, 5] // Объединяющая функция — должна быть каррирована const add = (x) => (y) => x + y const partiallyAppliedAdds = [add].ap(arg1) // [(y) => 1 + y, (y) => 3 + y]
Это дает массив функций, на котором можно вызвать ap для получения результата:
partiallyAppliedAdds.ap(arg2) // [5, 6, 7, 8]
Бифунктор
Структура с двумя независимыми параметрами типа, допускающая одновременное отображение по обоим параметрам. Бифунктор предоставляет метод bimap, который принимает две функции и применяет первую к первому параметру типа, а вторую — ко второму:
const Pair = (first, second) => ({ first, second, bimap: (f, g) => Pair(f(first), g(second)), firstMap: (f) => Pair(f(first), second), secondMap: (g) => Pair(first, g(second)) }) const score = Pair('alice', 10) score.bimap((name) => name.toUpperCase(), (points) => points * 2) // Pair('ALICE', 20)
Дополнительные материалы
Контравариантный функтор
Структура, подобная функтору, но в которой преобразование происходит в обратном направлении. Если ковариантный (covariant) функтор преобразует производителя (producer) F<A> в F<B> с помощью функции (a -> b), то контравариантный функтор преобразует потребителя (consumer) F<A> в F<B> с помощью функции (b -> a), используя метод cmap (или contramap).
Контравариантные функторы часто используются для моделирования предикатов, валидаторов, кодировщиков и компараторов сортировки, выполняя предварительную обработку входных данных перед их передачей потребителю.
// Predicate оборачивает тестовую функцию (x) -> Boolean const Predicate = (test) => ({ test, // cmap :: (b -> a) -> Predicate a -> Predicate b cmap: (f) => Predicate((x) => test(f(x))) }) // Существующий Predicate проверяет длину строки const isLongString = Predicate((s) => s.length > 5) // Contramap преобразует объект User в строку (user.bio) const hasLongBio = isLongString.cmap((user) => user.bio) hasLongBio.test({ bio: 'Hello World' }) // true hasLongBio.test({ bio: 'Hi' }) // false
Дополнительные материалы
Профунктор
Профунктор — это бифунктор, который контравариантен по первому аргументу и ковариантен по второму аргументу.
Для структуры P<A, B>, представляющей вычисление, которое потребляет A и выдает B, функция promap принимает две функции — (a' -> a) и (b -> b') — и возвращает P<A', B'>.
Функции вида (a -> b) являются каноническими профункторами: можно предварительно обработать входные данные с помощью (a' -> a) и пост-обработать выходные данные с помощью (b -> b'). Профункторы составляют математическую основу оптик на профункторах.
// Функции - натуральные профункторы: const Profunctor = (fn) => ({ run: fn, // promap :: (a' -> a) -> (b -> b') -> P a b -> P a' b' promap: (f, g) => Profunctor((x) => g(fn(f(x)))) }) // Существующая функция: String -> Number const stringLength = Profunctor((str) => str.length) // Предварительно обработанные входные данные (удаление пробелов) и пост-обработанные выходные данные (проверка на четность): const isTrimmedLengthEven = stringLength.promap( (raw) => raw.trim(), // контрвариант: предобработка аргумента (len) => len % 2 === 0 // ковариант: пост-обработка результата ) isTrimmedLengthEven.run(' code ') // 4 четное -> true isTrimmedLengthEven.run(' hello ') // 5 нечетное -> false
Дополнительные материалы
Альтернатива
Аппликативный функтор, который также образует моноид, предоставляя бинарный оператор выбора alt (часто записываемый как <|>) и нейтральный элемент для обработки сбоев и реализации резервной логики.
При объединении вычислений с помощью alt структура обычно реализует принцип “побеждает первый успешный вариант”: если предыдущее вычисление завершилось неудачей или вернуло пустой результат, управление переходит к следующим альтернативам.
const AltOption = { Some: (x) => ({ alt: (_other) => AltOption.Some(x), value: x }), None: () => ({ alt: (other) => other, value: null }) } // Цепочка конфигурации с механизмом резервирования: используется первое валидное значение const primaryConfig = AltOption.None() const secondaryConfig = AltOption.Some({ port: 8080 }) const defaultConfig = AltOption.Some({ port: 3000 }) const finalConfig = primaryConfig.alt(secondaryConfig).alt(defaultConfig) finalConfig.value // { port: 8080 }
Дополнительные материалы
Морфизм
Отношение между объектами одной категории. В контексте функционального программирования все функции являются морфизмами.
Гомоморфизм
Функция, обладающая структурным свойством, которое сохраняется неизменным при переходе от входных данных к выходным.
Например, в гомоморфизме моноидов и входные, и выходные данные являются моноидами, даже если их типы различаются:
// toList :: [number] -> string const toList = (a) => a.join(', ')
toList — это гомоморфизм, поскольку:
массив — это моноид: содержит операцию concat и идентичное значение ([])
строка — это моноид: содержит операцию concat и идентичное значение ('')
Таким образом, гомоморфизм связывает те свойства входных и выходных данных преобразования, которые нас интересуют.
Эндоморфизмы и изоморфизмы являются примерами гомоморфизма.
Дополнительные материалы
Эндоморфизм
Функция, у которой тип входных данных совпадает с типом выходных данных. Поскольку типы идентичны, эндоморфизмы также являются гомоморфизмами.
// uppercase :: String -> String const uppercase = (str) => str.toUpperCase() // decrement :: Number -> Number const decrement = (x) => x - 1
Изоморфизм
Морфизм, представляющий собой пару преобразований между объектами двух типов; он носит структурный характер, и при этом не происходит потери данных.
Например, двумерные координаты можно хранить в виде массива [2,3] или объекта {x: 2, y: 3}.
// Наличие функций для преобразования в обоих направлениях делает структуры двумерных координат изоморфными const pairToCoords = (pair) => ({ x: pair[0], y: pair[1] }) const coordsToPair = (coords) => [coords.x, coords.y] coordsToPair(pairToCoords([1, 2])) // [1, 2] pairToCoords(coordsToPair({ x: 1, y: 2 })) // {x: 1, y: 2}
Изоморфизмы — интересный пример морфизма, поскольку для него требуется нечто большее, чем просто одна функция. Изоморфизмы также являются гомоморфизмами, так как типы входных и выходных данных обладают свойством обратимости.
Катаморфизм
Функция, которая преобразует структуру в простое значение. reduceRight — это пример катаморфизма для массивов:
// sum - это катаморфизм из [Number] -> Number const sum = xs => xs.reduceRight((acc, x) => acc + x, 0) sum([1, 2, 3, 4, 5]) // 15
Анаморфизм
Функция, которая формирует структуру путем многократного применения функции к своему аргументу. Примером служит unfold, генерирующая массив на основе функции и начального значения. Это операция, обратная катаморфизму. Можно сказать, что анаморфизм создает структуру, а катаморфизм — разбирает ее.
const unfold = (f, seed) => { function go (f, seed, acc) { const res = f(seed) return res ? go(f, res[1], acc.concat([res[0]])) : acc } return go(f, seed, []) }
const countDown = n => unfold((n) => { return n <= 0 ? undefined : [n, n - 1] }, n) countDown(5) // [5, 4, 3, 2, 1]
Гиломорфизм
Функция, которая компонует анаморфизм и следующий за ним катаморфизм.
const sumUpToX = (x) => sum(countDown(x)) sumUpToX(5) // 15
Параморфизм
Функция, подобная reduceRight. Однако есть отличие — в параморфизме аргументами функции-редуктора являются текущее значение, результат свертки всех предыдущих значений и список значений, из которых этот результат был получен:
// Небезопасно для списков, содержащих `undefined`, // но вполне подходит для иллюстрации const para = (reducer, accumulator, elements) => { if (elements.length === 0) { return accumulator } const head = elements[0] const tail = elements.slice(1) return reducer(head, tail, para(reducer, accumulator, tail)) } const suffixes = list => para( (x, xs, suffxs) => [xs, ...suffxs], [], list ) suffixes([1, 2, 3, 4, 5]) // [[2, 3, 4, 5], [3, 4, 5], [4, 5], [5], []]
Третий параметр в редукторе (в приведенном примере - [x, ... xs]) — это своего рода история того, как было получено текущее значение acc.
Апоморфизм
Это противоположность параморфизма — точно так же, как анаморфизм противоположен катаморфизму. В то время как параморфизм сохраняет доступ к аккумулятору и накопленным данным, апоморфизм позволяет выполнять развертывание (unfold) с возможностью досрочного завершения.
Естественное преобразование
Отображение между двумя функторами, сохраняющее структуру и преобразующее F<A> в G<A> без изменения или анализа содержащегося внутри значения A.
В функциональном программировании естественное преобразование — это функция, которая меняет тип контейнера, сохраняя его содержимое и удовлетворяя закону естественности: nat(fa.map(f)) === nat(fa).map(f).
// nat :: F a -> G a // Например, массив в Option (берем головной элемент) const listToOption = (arr) => (arr.length > 0 ? { value: arr[0], isSome: true } : { value: null, isSome: false }) const double = (x) => x * 2 // Закон естественности: преобразование после отображения равно отображению после преобразования const arrayTransformed = listToOption([1, 2, 3].map(double)) // { value: 2, isSome: true } const mappedOption = { value: double(listToOption([1, 2, 3]).value), isSome: true } // { value: 2, isSome: true } arrayTransformed.value === mappedOption.value // true
Дополнительные материалы
Естественное преобразование в Википедии
Сетоид
Объект, имеющий функцию equals, которую можно использовать для сравнения других объектов того же типа.
Делаем массив сетоидом:
Array.prototype.equals = function (arr) { const len = this.length if (len !== arr.length) { return false } for (let i = 0; i < len; i++) { if (this[i] !== arr[i]) { return false } } return true } ;[1, 2].equals([1, 2]) // true ;[1, 2].equals([0]) // false
Полугруппа
Объект, содержащий функцию concat, которая комбинирует его с другим объектом такого же типа:
;[1].concat([2]) // [1, 2]
Свертка
Объект, имеющий функцию reduce, которая применяет заданную функцию к аккумулятору и каждому элементу массива (слева направо), сводя его к единственному значению:
const sum = (list) => list.reduce((acc, val) => acc + val, 0) sum([1, 2, 3]) // 6
Проходимый/обходимый?
Тип, являющийся одновременно сверткой и функтором, который позволяет “вывернуть наизнанку” коллекцию вложенных значений с помощью методов sequence или traverse, преобразуя F<G<A>> в G<F<A>>.
Этот механизм часто используется для обработки списка асинхронных операций или значений, допускающих отсутствие результата (nullable), когда требуется вынести эффект, создаваемый внешней оболочкой, на более высокий уровень:
// promiseSequence преобразует список промисов в промис, возвращающий список // [Promise<1>, Promise<2>] -> Promise<[1, 2]> const promiseSequence = (promises) => promises.reduce( (acc, p) => acc.then((arr) => p.then((val) => [...arr, val])), Promise.resolve([]) ) promiseSequence([ Promise.resolve(1), Promise.resolve(2), Promise.resolve(3) ]).then(console.log) // [1, 2, 3]
Дополнительные материалы
Объектив/линза
Линза — это структура (часто объект или функция), объединяющая геттер и немутирующий сеттер для некоторой другой структуры данных:
// С помощью [лизны Ramda](http://ramdajs.com/docs/#lens) const nameLens = R.lens( // Геттер для свойства name объекта (obj) => obj.name, // Сеттер для свойства name (val, obj) => Object.assign({}, obj, { name: val }) )
Наличие пары методов get и set для конкретной структуры данных открывает ряд интересных возможностей:
const person = { name: 'Gertrude Blanch' } // Вызов геттера R.view(nameLens, person) // 'Gertrude Blanch' // Вызов сеттера R.set(nameLens, 'Shafi Goldwasser', person) // {name: 'Shafi Goldwasser'} // Запуск функции на значении структуры R.over(nameLens, uppercase, person) // {name: 'GERTRUDE BLANCH'}
Линзы также компонуемы. Это облегчает иммутабельные обновления глубоко вложенных данных:
// Эта линза фокусируется на первом элементе непустого массива const firstLens = R.lens( // Извлекаем первый элемент массива xs => xs[0], // Иммутабельный сеттер для первого элемента массива (val, [__, ...xs]) => [val, ...xs] ) const people = [{ name: 'Gertrude Blanch' }, { name: 'Shafi Goldwasser' }] // Линзы компонуются слева направо R.over(compose(firstLens, nameLens), uppercase, people) // [{'name': 'GERTRUDE BLANCH'}, {'name': 'Shafi Goldwasser'}]
Другие реализации:
partial.lenses — приятный синтаксический сахар и множество мощных возможностей
nanoscope — текучий (fluent) интерфейс
Призма
Оптика, фокусирующаяся на подтипе или варианте суммарного типа. В отличие от линзы, которая всегда предполагает наличие целевого поля в финальном типе (product type), призма может не сработать, так как целевой вариант может отсутствовать.
Призма состоит из функции preview (которая возвращает опцию или null) и функции review (которая реконструирует всю структуру данных из части, находящейся в фокусе):
const Prism = (preview, review) => ({ preview, review }) // Призма, фокусируемая на числах в строках const integerPrism = Prism( (str) => (/^-?\d+$/.test(str) ? Number(str) : null), (num) => String(num) ) integerPrism.preview('42') // 42 integerPrism.preview('hello') // null integerPrism.review(42) // '42'
Дополнительные материалы
Оптический объект, определяющий взаимно-однозначное (биективное) и обратимое соответствие без потерь между двумя представлениями одной и той же информации (s и a). Iso состоит из функции to (типа s -> a) и функции from (типа a -> s), для которых выполняются условия from(to(x)) === x и to(from(y)) === y.
Iso лежат в основе обратимых преобразований, таких как перевод температурных шкал, смена систем координат или кодирование и декодирование структур данных.
const Iso = (to, from) => ({ to, from }) // Преобразование между градусами Цельсия и градусами Фаренгейта const tempIso = Iso( (c) => (c * 9) / 5 + 32, // в градусы Фаренгейта (f) => ((f - 32) * 5) / 9 // из градусов Фаренгейта ) tempIso.to(100) // 212 tempIso.from(212) // 100
Дополнительные материалы
Изоморфизм в Википедии
Optics / Iso на GitHub
Обход
Оптика, которая одновременно фокусируется на нуле, одном или нескольких значениях (0..*) внутри структуры данных.
В то время как линза фокусируется ровно на одном значении, а призма - на нуле или одном значении, обход обобщает понятие оптики для работы с коллекциями, деревьями или отфильтрованными подмножествами.
Обход предоставляет:
getAll: извлекает все искомые значения в массив.
modify: иммутабельно преобразует каждое искомое значение с помощью связывающей (mapping) функции
const Traversal = (getAll, modify) => ({ getAll, modify }) // Обход фокусируется только на четных числах в массиве: const evenTraversal = Traversal( (arr) => arr.filter((n) => n % 2 === 0), (f, arr) => arr.map((n) => (n % 2 === 0 ? f(n) : n)) ) const numbers = [1, 2, 3, 4, 5, 6] evenTraversal.getAll(numbers) // [2, 4, 6] evenTraversal.modify((n) => n * 10, numbers) // [1, 20, 3, 40, 5, 60]
Дополнительные материалы
Сигнатуры типов
Часто функции в JavaScript включают комментарии с типами их аргументов и возвращаемых значений.
Шаблон часто выглядит так:
// functionName :: firstArgType -> secondArgType -> returnType // add :: Number -> Number -> Number const add = (x) => (y) => x + y // increment :: Number -> Number const increment = (x) => x + 1
Если функция принимает другую функцию в качестве аргумента, последняя оборачивается в скобки:
// call :: (a -> b) -> a -> b const call = (f) => (x) => f(x)
Буквы a, b, c, d используются для обозначения того, что аргумент может быть любого типа. Приведенная ниже версия функции map принимает функцию, преобразующую значение типа a в значение типа b, а также массив значений типа a, и возвращает массив значений типа b.
// map :: (a -> b) -> [a] -> [b] const map = (f) => (list) => list.map(f)
Дополнительные материалы
Алгебраический тип данных
Составной тип, образованный путем объединения других типов. К числу распространенных видов алгебраических типов относятся типы-суммы и типы-произведения.
Тип-сумма
Тип-сумма (sum type) — это объединение двух типов в один новый тип. Он называется суммой, поскольку количество возможных значений результирующего типа равно сумме количеств значений исходных типов.
В JavaScript нет таких типов, но мы можем имитировать их поведение с помощью Set:
// Представьте, что вместо множеств у нас есть типы, // которые могут иметь только определенные значения const bools = new Set([true, false]) const halfTrue = new Set(['half-true']) // Тип weakLogic содержит сумму значений из bools и halfTrue const weakLogicValues = new Set([...bools, ...halfTrue])
Типы-суммы иногда называют типами-объединениями (union types), исключающими объединениями (discriminated unions) или тегированными объединениями (tagged unions).
В JavaScript существует несколько библиотек, упрощающих определение и использование типов-объединений.
Flow предоставляет типы-объединения, а в TypeScript для решения той же задачи используются перечисления (enums).
Тип-произведение
Тип-произведение объединяет типы более привычным способом:
// point :: (Number, Number) -> {x: Number, y: Number} const point = (x, y) => ({ x, y })
Это называется “произведением”, поскольку общее количество возможных значений такой структуры данных представляет собой произведение количества различных значений. Во многих языках программирования существует тип кортеж (tuple), являющийся простейшей реализацией типа-произведения.
Дополнительные материалы
Теория множеств в Википедии
Опция
Опция — это тип-сумма с двумя вариантами, часто называемыми Some и None.
Опция полезна для компоновки функций, которые не всегда возвращают значение:
// Наивное определение const Some = (v) => ({ val: v, map (f) { return Some(f(this.val)) }, chain (f) { return f(this.val) } }) const None = () => ({ map (f) { return this }, chain (f) { return this } }) // maybeProp :: (String, {a}) -> Option a const maybeProp = (key, obj) => typeof obj[key] === 'undefined' ? None() : Some(obj[key])
Используем chain для создания последовательности функций, возвращающих Option:
// getItem :: Cart -> Option CartItem const getItem = (cart) => maybeProp('item', cart) // getPrice :: Item -> Option Number const getPrice = (item) => maybeProp('price', item) // getNestedPrice :: cart -> Option a const getNestedPrice = (cart) => getItem(cart).chain(getPrice) getNestedPrice({}) // None() getNestedPrice({ item: { foo: 1 } }) // None() getNestedPrice({ item: { price: 9.99 } }) // Some(9.99)
Option также известна, как Maybe. Some иногда называется Just, а None - Nothing.
Или
Тип-сумма с двумя вариантами, Left и Right. По соглашению, Right представляет успешное вычисление, а Left содержит ошибку или причину провала (“right is right” — правый есть правильный).
Either полезен для обработки ошибок без исключений, позволяя вычислениям мягко (gracefully) проваливаться, оставаясь чистыми и компонуемыми:
const Left = (x) => ({ value: x, map: (_f) => Left(x), chain: (_f) => Left(x), fold: (f, _g) => f(x), isLeft: true }) const Right = (x) => ({ value: x, map: (f) => Right(f(x)), chain: (f) => f(x), fold: (_f, g) => g(x), isRight: true }) // parseJson :: String -> либо String Object const parseJson = (str) => { try { return Right(JSON.parse(str)) } catch (err) { return Left(err.message) } } parseJson('{"user": "hemanth"}').map((obj) => obj.user) // Right('hemanth') parseJson('invalid json').map((obj) => obj.user) // Left('Unexpected token...')
Дополнительные материалы
Функция
Функция f :: A => B — это выражение (часто называемое стрелочным или лямбда-выражением), принимающее ровно один (неизменяемый) параметр типа A и возвращающее ровно одно значение типа B. Это значение полностью определяется аргументом, благодаря чему функции не зависят от контекста и обладают свойством ссылочной прозрачности. Подразумевается, что функция не должна вызывать никаких скрытых побочных эффектов; по определению, такая функция всегда является чистой. Эти свойства делают работу с функциями удобной: они полностью детерминированы и, следовательно, предсказуемы. Функции позволяют работать с кодом как с данными, обеспечивая абстрагирование от поведения:
// times2 :: Number -> Number const times2 = n => n * 2 ;[1, 2, 3].map(times2) // [2, 4, 6]
Частичная функция
Частичная функция — это функция, которая определена не для всех аргументов: она может вернуть неожиданный результат или вообще не завершить выполнение. Частичные функции создают дополнительную когнитивную нагрузку, их сложнее анализировать, и они могут приводить к ошибкам во время выполнения. Вот несколько примеров:
// Пример 1: сумма списка // sum :: [Number] -> Number const sum = arr => arr.reduce((a, b) => a + b) sum([1, 2, 3]) // 6 sum([]) // TypeError: Reduce of empty array with no initial value // Пример 2: получение первого элемента списка // first :: [A] -> A const first = a => a[0] first([42]) // 42 first([]) // undefined // Или даже хуже: first([[42]])[0] // 42 first([])[0] // Uncaught TypeError: Cannot read property '0' of undefined // Пример 3: выполнение функции N раз // times :: Number -> (Number -> Number) -> Number const times = n => fn => n && (fn(n), times(n - 1)(fn)) times(3)(console.log) // 3 // 2 // 1 times(-1)(console.log) // RangeError: Maximum call stack size exceeded
Работа с частичными функциями
Частичные функции опасны, и обращаться с ними следует с большой осторожностью. Можно получить неожиданный (неверный) результат или столкнуться с ошибками во время выполнения. Иногда частичная функция может вообще не вернуть никакого значения. Учет всех подобных граничных случаев и соответствующая их обработка могут стать весьма утомительным занятием. К счастью, частичную функцию можно преобразовать в обычную (или тотальную (total)). Мы можем задать значения по умолчанию или использовать “защитников” (guards) для обработки входных данных, для которых исходная частичная функция не была определена. Используя тип Option, мы можем возвращать Some(value) или None в тех ситуациях, когда в противном случае поведение программы было бы непредсказуемым:
// Пример 1: сумма списка // Можно предоставить дефолтное значение, чтобы функция всегда возвращала какой-то результат // sum :: [Number] -> Number const sum = arr => arr.reduce((a, b) => a + b, 0) sum([1, 2, 3]) // 6 sum([]) // 0 // Пример 2: получение первого элемента списка // Меняем результат на Option // first :: [A] -> Option A const first = a => a.length ? Some(a[0]) : None() first([42]).map(a => console.log(a)) // 42 first([]).map(a => console.log(a)) // console.log не будет выполнен // Предыдущий худший результат first([[42]]).map(a => console.log(a[0])) // 42 first([]).map(a => console.log(a[0])) // не будет выполнен, поэтому не возникнет ошибки // Благодаря типу возвращаемого значения (Option) мы понимаем, // что для доступа к данным нужно использовать метод `.map`, // и никогда не забудем проверить входные данные, // поскольку такая проверка встроена в саму функцию // Пример 3: выполнение функции N раз // Следует обеспечить обязательное завершение функции путем изменения условий: // times :: Number -> (Number -> Number) -> Number const times = n => fn => n > 0 && (fn(n), times(n - 1)(fn)) times(3)(console.log) // 3 // 2 // 1 times(-1)(console.log) // Ничего не будет выполнено
Преобразование частичных функций в полные позволяет предотвратить подобные ошибки времени выполнения. Кроме того, гарантированный возврат значения делает код более простым в сопровождении и анализе.
Полная фукнция
Функция, возвращающая корректный результат для всех входных данных, определенных в ее типе. Это противоположность частичных функций, которые могут вызвать ошибку, вернуть неожиданный результат или не завершить выполнение.
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩
