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

Примеры написаны на JavaScript (ES2015). Почему JavaScript?

Где уместно, в этом документе используются термины, определенные в Fantasy Land spec.

Содержание

❯ Arity

Арность

Количество аргументов, принимаемых функцией. От слов, вроде “унарный”, “бинарный”, “тернарный” и т.п.

const sum = (a, b) => a + b
// Арность sum - 2 (бинарный)
const inc = a => a + 1
// Арность inc - 1 (унарный)
const zero = () => 0
// Арность zero - 0 (нульарный)

Дополнительные материалы

❯ Higher-Order Function (HOF)

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

Функция, принимающая другую функцию в качестве аргумента и/или возвращающая функцию:

const filter = (predicate, xs) => xs.filter(predicate)
const is = (type) => (x) => Object(x) instanceof type
filter(is(Number), [0, '1', 2, null]) // [0, 2]

❯ Closure

Замыкание

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

const addTo = x => y => x + y
const addToFive = addTo(5)
addToFive(3) // => 8

В данном случае x сохраняется в замыкании addToFive со значением 5. addToFive затем может вызываться с y для получения суммы.

Дополнительные материалы

❯ Partial Application

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

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

// Утилита для создания частично примененных функций.
// Принимает функцию и аргументы
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

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

❯ Currying

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

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

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

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

❯ Auto Currying

Автокаррирование

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

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

Дополнительные материалы

❯ Function Composition

Композиция функций

Объединение двух функций в одну, где результат одной функции передается на вход другой функции. Эта одна из наиболее важных концепций ФП.

const compose = (f, g) => (a) => f(g(a)) // Определение
const floorAndToString = compose((val) => val.toString(), Math.floor) // Использование
floorAndToString(121.212121) // '121'

❯ Continuation

Продолжение

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

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

Чистая структура данных, которая инкапсулирует сторонний эффект. Вместо выполнения эффекта сразу, 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()

Дополнительные материалы

❯ Trampoline

Трамплин/батут

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

В средах выполнения без 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

Дополнительные материалы

❯ Thunk

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

// Жадное (eager) вычисление выполняется сразу:
// const data = expensiveCalculation()

// Thunk оборачивает выражение в функцию, откладывая ее выполнение:
const thunk = () => 42 * 2

// Выражение вычисляется только при явном вызове:
thunk() // 84

Дополнительные материалы

❯ Algebraic Effects

Алгебраические эффекты

Система вычислительных эффектов, которая отделяет вызов эффекта от его обработки. Вместо привязки функции к среде выполнения, функция “выполняет” операцию эффекта (такую как чтение состояния, запрос конфигурации или логгирование). Внешний “обработчик” перехватывает выполняемый эффект и предоставляет результат, позволяя возобновить вычисления или прервать их; это обобщает механизмы исключений, 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()
    }
  }
)

Дополнительные материалы

❯ Pure Function

Чистая функция

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

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"

Здесь модифицируется состояние, не принадлежащее функции.

❯ Side effects

Сторонние эффекты

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

const differentEveryTime = new Date()
console.log('IO is a side effect!')

❯ Idempotence

Идемпотентность

Функция является идемпонтентной, если ее повторное применение всегда дает одинаковый результат:

Math.abs(Math.abs(10))
sort(sort(sort([2, 1])))

❯ Point-Free Style

Беспараметрический стиль

Написание функций, где дефиниция не определяет используемые аргументы явно. Этот стиль обычно требует каррирования или других функций более высокого порядка. Другое название — бесточечное программирование (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 или =>. Следует отметить, что беспараметрические функции не обязательно лучше обычных, поскольку они могут быть более сложными в понимании в сложных случаях.

❯ Predicate

Предикат

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

const predicate = (a) => a > 2

;[1, 2, 3, 4].filter(predicate) // [3, 4]

❯ Contract

Контракт

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

// Определяем наш контракт: 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

❯ Category

Категория

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

Три правила валидной категории:

  1. Должен существовать морфизм тождества, отображающий объект в самого себя. Если a — объект некоторой категории, то должно существовать отображение a -> a.

  2. Морфизмы должны обладать свойством композиции. Если a, b и c — объекты некоторой категории, f — морфизм из a в b, а g — морфизм из b в c, то g(f(x)) должно быть эквивалентно (g • f)(x).

  3. Композиция должна быть ассоциативной: 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)

Дополнительные материалы

❯ Semigroupoid

Полугруппоид

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

Полугруппоид удовлетворяет свойству ассоциативности для композиции: 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!'

Дополнительные материалы

❯ Value

Значение

Все, что может быть присвоено переменной:

5
Object.freeze({ name: 'John', age: 30 }) // функция `freeze` обеспечивает иммутабельность
;(a) => a
;[1]
undefined

❯ Constant

Константа

Переменная, значение которой не может меняться после определения:

const five = 5
const john = Object.freeze({ name: 'John', age: 30 })

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

Следующее выражение всегда возвращает true:

john.age + five === ({ name: 'John', age: 30 }).age + 5

Constant Function

Константная функция

Каррированая функция, игнорирующая второй аргумент:

const constant = a => () => a

;[1, 2].map(constant(0)) // => [0, 0]

Constant Functor

Константный функтор

Объект, чей метод map не модифицирует его содержимое. См. функтор.

Constant(1).map(n => n + 1) // => Constant(1)

Constant Monad

Константная монада

Объект, чем метод chain не модифицирует его содержимое. См. монада.

Constant(1).chain(n => Constant(n + 1)) // => Constant(1)

❯ Functor

Функтор

Объект, реализующий функцию 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)

❯ Pointed Functor

Пунктированный функтор

Это объект с функцией of, которая добавляет в него любое единичное значение.

ES2015 добавил метод Array.of, сделав массив пунктированным функтором:

Array.of(1) // [1]

❯ Lift

Подъем/поднятие

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

Некоторые реализации имеют функцию под названием 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]

❯ Referential Transparency

Ссылочная прозрачность

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

Рассмотрим такую функцию:

const greet = () => 'Hello World!'

Любой вызов greet() можно заменить на Hello World!, следовательно, функция greet обладает свойством референциальной прозрачности. Это свойство было бы нарушено, если бы greet зависела от внешнего состояния, например, от конфигурации или обращения к базе данных. См. также чистые функции и рассуждение на основе равенств.

❯ Equational Reasoning

Рассуждение на основе равенств

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

const grainToDogs = compose(chickenIntoDogs, grainIntoChicken)
const grainToCats = compose(dogsIntoCats, grainToDogs)

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

❯ Memoization

Мемоизация

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

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

Дополнительные материалы

❯ Lambda

Лямбда

Анонимная функция, с которой можно обращаться как со значением:

;(function (a) {
  return a + 1
})

;(a) => a + 1

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

;[1, 2].map((a) => a + 1) // [2, 3]

Лямбду можно присвоить переменной:

const add1 = (a) => a + 1

❯ Lambda Calculus

Лямбда-исчисление

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

❯ Functional Combinator

Функциональный комбинатор

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

// Комбинатор "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.

❯ Lazy evaluation

Ленивое вычисление

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

const rand = function * () {
  while (1 < 2) {
    yield Math.random()
  }
}
const randIter = rand()
randIter.next() // каждое выполнение дает произвольное значение, выражение оценивается по требованию

❯ Monoid

Моноид

Объект с функцией, которая “комбинирует” этот объект с другим объектом того же типа (полугруппа), который имеет “идентичное” значение.

Примером простого моноида является сложение чисел:

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

❯ Monad

Монада

Монада — это объект с функциями 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.

❯ Comonad

Комонада

Объект, содержащий функции 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)

❯ Kleisli Composition

Композиция Клейсли

Операция композиции двух функций, возвращающих монаду (стрелки Клейсли), при условии совместимости их типов. В 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.

❯ Free Monad

Свободная монада

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

Свободная монада бывает двух видов:

  • 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

Дополнительные материалы

❯ Monad Transformer

Трансформер монад

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

Названия трансформеров монад, обычно, заканчивается на 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 }

Дополнительные материалы

❯ Applicative Functor

Аппликативный функтор

Аппликативный функтор — это объект с функцией 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]

❯ Bifunctor

Бифунктор

Структура с двумя независимыми параметрами типа, допускающая одновременное отображение по обоим параметрам. Бифунктор предоставляет метод 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)

Дополнительные материалы

❯ Contravariant Functor

Контравариантный функтор

Структура, подобная функтору, но в которой преобразование происходит в обратном направлении. Если ковариантный (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

Дополнительные материалы

❯ Profunctor

Профунктор

Профунктор — это бифунктор, который контравариантен по первому аргументу и ковариантен по второму аргументу.

Для структуры 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

Дополнительные материалы

❯ Alternative

Альтернатива

Аппликативный функтор, который также образует моноид, предоставляя бинарный оператор выбора 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 }

Дополнительные материалы

❯ Morphism

Морфизм

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

Homomorphism

Гомоморфизм

Функция, обладающая структурным свойством, которое сохраняется неизменным при переходе от входных данных к выходным.

Например, в гомоморфизме моноидов и входные, и выходные данные являются моноидами, даже если их типы различаются:

// toList :: [number] -> string
const toList = (a) => a.join(', ')

toList — это гомоморфизм, поскольку:

  • массив — это моноид: содержит операцию concat и идентичное значение ([])

  • строка — это моноид: содержит операцию concat и идентичное значение ('')

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

Эндоморфизмы и изоморфизмы являются примерами гомоморфизма.

Дополнительные материалы

Endomorphism

Эндоморфизм

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

// uppercase :: String -> String
const uppercase = (str) => str.toUpperCase()

// decrement :: Number -> Number
const decrement = (x) => x - 1

Isomorphism

Изоморфизм

Морфизм, представляющий собой пару преобразований между объектами двух типов; он носит структурный характер, и при этом не происходит потери данных.

Например, двумерные координаты можно хранить в виде массива [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}

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

Catamorphism

Катаморфизм

Функция, которая преобразует структуру в простое значение. reduceRight — это пример катаморфизма для массивов:

// sum - это катаморфизм из [Number] -> Number
const sum = xs => xs.reduceRight((acc, x) => acc + x, 0)

sum([1, 2, 3, 4, 5]) // 15

Anamorphism

Анаморфизм

Функция, которая формирует структуру путем многократного применения функции к своему аргументу. Примером служит 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]

Hylomorphism

Гиломорфизм

Функция, которая компонует анаморфизм и следующий за ним катаморфизм.

const sumUpToX = (x) => sum(countDown(x))
sumUpToX(5) // 15

Paramorphism

Параморфизм

Функция, подобная 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.

Apomorphism

Апоморфизм

Это противоположность параморфизма — точно так же, как анаморфизм противоположен катаморфизму. В то время как параморфизм сохраняет доступ к аккумулятору и накопленным данным, апоморфизм позволяет выполнять развертывание (unfold) с возможностью досрочного завершения.

❯ Natural Transformation

Естественное преобразование

Отображение между двумя функторами, сохраняющее структуру и преобразующее 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

Дополнительные материалы

❯ Setoid

Сетоид

Объект, имеющий функцию 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

❯ Semigroup

Полугруппа

Объект, содержащий функцию concat, которая комбинирует его с другим объектом такого же типа:

;[1].concat([2]) // [1, 2]

❯ Foldable

Свертка

Объект, имеющий функцию reduce, которая применяет заданную функцию к аккумулятору и каждому элементу массива (слева направо), сводя его к единственному значению:

const sum = (list) => list.reduce((acc, val) => acc + val, 0)
sum([1, 2, 3]) // 6

❯ Traversable

Проходимый/обходимый?

Тип, являющийся одновременно сверткой и функтором, который позволяет “вывернуть наизнанку” коллекцию вложенных значений с помощью методов 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]

Дополнительные материалы

❯ Lens

Объектив/линза

Линза — это структура (часто объект или функция), объединяющая геттер и немутирующий сеттер для некоторой другой структуры данных:

// С помощью [лизны 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) интерфейс

❯ Prism

Призма

Оптика, фокусирующаяся на подтипе или варианте суммарного типа. В отличие от линзы, которая всегда предполагает наличие целевого поля в финальном типе (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'

Дополнительные материалы

❯ Iso

Оптический объект, определяющий взаимно-однозначное (биективное) и обратимое соответствие без потерь между двумя представлениями одной и той же информации (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

Дополнительные материалы

❯ Traversal

Обход

Оптика, которая одновременно фокусируется на нуле, одном или нескольких значениях (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]

Дополнительные материалы

❯ Type Signatures

Сигнатуры типов

Часто функции в 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)

Дополнительные материалы

❯ Algebraic data type

Алгебраический тип данных

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

Sum type

Тип-сумма

Тип-сумма (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).

Product type

Тип-произведение

Тип-произведение объединяет типы более привычным способом:

// point :: (Number, Number) -> {x: Number, y: Number}
const point = (x, y) => ({ x, y })

Это называется “произведением”, поскольку общее количество возможных значений такой структуры данных представляет собой произведение количества различных значений. Во многих языках программирования существует тип кортеж (tuple), являющийся простейшей реализацией типа-произведения.

Дополнительные материалы

❯ Option

Опция

Опция — это тип-сумма с двумя вариантами, часто называемыми 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.

❯ Either

Или

Тип-сумма с двумя вариантами, 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...')

Дополнительные материалы

❯ Function

Функция

Функция f :: A => B — это выражение (часто называемое стрелочным или лямбда-выражением), принимающее ровно один (неизменяемый) параметр типа A ​​и возвращающее ровно одно значение типа B. Это значение полностью определяется аргументом, благодаря чему функции не зависят от контекста и обладают свойством ссылочной прозрачности. Подразумевается, что функция не должна вызывать никаких скрытых побочных эффектов; по определению, такая функция всегда является чистой. Эти свойства делают работу с функциями удобной: они полностью детерминированы и, следовательно, предсказуемы. Функции позволяют работать с кодом как с данными, обеспечивая абстрагирование от поведения:

// times2 :: Number -> Number
const times2 = n => n * 2

;[1, 2, 3].map(times2) // [2, 4, 6]

❯ Partial function

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

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

// Пример 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

Dealing with partial functions

Работа с частичными функциями

Частичные функции опасны, и обращаться с ними следует с большой осторожностью. Можно получить неожиданный (неверный) результат или столкнуться с ошибками во время выполнения. Иногда частичная функция может вообще не вернуть никакого значения. Учет всех подобных граничных случаев и соответствующая их обработка могут стать весьма утомительным занятием. К счастью, частичную функцию можно преобразовать в обычную (или тотальную (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)
// Ничего не будет выполнено

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

❯ Total function

Полная фукнция

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

❯ Библиотеки функционального программирования на JavaScript


Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩

Комментарии (0)