S.Tominoff

Frontend / Backend developer

Каррирование в 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]

 

Рассмотрим что тут происходит более конкретно:

 

  1. R.filter — аналогичен Array.prototype.map, но каррированный — фильтрует данные с помощью функции-предиката. Второй аргумент функции это данные которые нужно отфильтровать
  2. R.complement — булева функция, возвращает противоположное своему аргументу значение, для true вернёт false и наоборот, по сути логическое отрицание
  3. R.either — булева функция работает аналогично булевой операции ИЛИ
  4. 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>
}