Каррирование в Javascript
Каррирование — одна из основных концепций применяемых в функциональном программировании. В этой статье кратко рассмотрим что это и зачем и как можно применять карринг в Javascript.
Что такое каррирование функций?
Суть каррирования проста и заключается в преобразовании функции с любой арностью (количеством ожидаемых аргументов) в унарную функцию.
В самом простом случае, если у нас есть функция вида:
(a, b) => a + b
каррированная будет такой:
(a) => (b) => a + b
Это пример строгого каррирования, при котором функция всегда становится унарной. Подобную реализацию вы можете встретить например в santuary.js.
В функциональных языках программирования обычно используется строгое каррирование, однако JavaScript синтаксически менее располагает к использованию таких функций, поэтому во многих реализациях функции curry, каррированная функция менее строгая и позволяет передавать сразу несколько аргументов.
Зачем нужно каррирование?
Основная прелесть карринга, естественно, проявляется при использовании подходов из функционального программирования, но сейчас рассмотрим простейший пример.
Допустим у нас есть некий список объектов, нужно создать функцию для получения из каждого элемента списка некого свойства.
Вот пример реализации при обычном императивном подходе к вопросу:
const list = [
{ id: 1, name: 'Пётр' },
{ id: 2, name: 'Василий' },
{ id: 3, name: 'Сергей' }
]
const getIds = (list) => {
return list.map((item) => {
return item.id
})
}
const ids = getIds(list) // [ 1, 2, 3 ]
Выглядит вполне обычно, думаю ни для кого не в новинку такой код — все так писали или пишут.
А теперь давайте посмотрим как можно сделать его лучше, используя карринг функций 😉.
Для начала давайте запилим две вспомогательные функции для работы с данными:
// функция возвращает значение свойства объекта
const pick = curry((field, obj) => obj[field])
// обёртка над array.prototype.map
const map = curry((fn, array) => array.map(fn))
Ничего особенного — просто каррированный геттер для свойств объекта и каррированная версия метода map.
Используя эти функции наш код становится куда более лаконичным:
const list = [
{ id: 1, name: 'Пётр' },
{ id: 2, name: 'Василий' },
{ id: 3, name: 'Сергей' }
]
const getIds = map(pick('id'))
const ids = getIds(list) // [ 1, 2, 3 ]
Яркий пример выгоды каррированных функций можно продемонстрировать на примере использования библиотеки Ramda, в которой вообще все функции каррированы.
Благодаря этой особенности, вместо описания конретной логики работы с конкретными данными, как это обычно происходит при имеративном подходе — мы пишем рецепты путём комбинирования функций.
При работе с этой библиотекой мы думаем не о конкретных данных, а об алгоритме функции. Вот простой пример:
// функция для удаления всех пустых и null значений из массива
const cleanArray =
R.filter(
R.complement(
R.either(R.isNil, R.isEmpty)
)
)
cleanArray([null, '', 'hello', undefined, '', '123', 0, 1]) // -> ["hello", "123", 0, 1]
Разберём что тут происходит:
- R.filter — аналог стандартной функции Array.prototype.filter, но при этом каррированный. Первым аргументом нужно передать функцию-предикат с логикой фильтрации, второй аргумент функции – данные которые нужно отфильтровать
- R.complement — булева функция, возвращает противоположное своему аргументу значение, для true вернёт false и наоборот, по сути логическое отрицание
- R.either — булева функция работает аналогично булевой операции ИЛИ
- R.isNil, R.isEmpty — функции проверяют является ли аргумент null или пустым значением
Мы написали рабочую функцию, но при этом нигде не описывали ни получаемые аргументы, ни то куда и как их применить, мы просто описали рецепт обработки данных.
Ramda не заставляет нас сразу же передавать в функции данные для обработки и обрабатывать их на месте — вместо этого мы только лишь описываем порядок действий над данными которые появятся в будущем.
В чём плюсы карринга?
С помощью каррирования, мы получаем возможность разбивать сложные функции с множеством аргументов — естественно это позволяет легко переиспользовать код.
Более важным преимуществом каррированных функций заключается в возможности применять их в функциональной композиции.
Мы не станем сейчас углубляться в композицию, но если кратко — она заключается в передаче в качестве аргумента результата вызова предыдущей функции. Иначе говоря — compose(f, g) -> g(f).
Используя карринг и композицию вы можете строить сложную логику с помощью простых функций.
Пример из жизни
В бэкенде сервиса Мозаика.ЕДА мы активно используем композицию каррированных функций для удобного расширения поведения ручек GraphQL.
К примеру, мы легко можем добавить кэширование или проверку передаваемых аргументов с фронтенд приложения к любой ручке используя такой паттерн:
Кроме того, каррирование зачастую может помочь и на фронтенде. Например, мы можем создать частично применённую функцию на основе карринга для обработчиков событий:
import React, { useState, useMemo } from 'react'
import { flip, mergeLeft, curry } from 'ramda'
export default function Component () {
const [ state, setState ] = useState({})
const onChange = useMemo(() => curry((field, event) => {
// setState(state => mergeLeft(state, ...))
setState(flip(mergeLeft)({ [field]: event.target.value })
}), [])
return <div>
<input value={state.name} onChange={onChange('name')} />
<input value={state.email} onChange={onChange('email')} />
</div>
}