Различные виды каррирования и способы их использования для решения проблем

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

Мне не нужен весь мир, только ты

Давайте посмотрим на пример функции карри в JavaScript:

const curry = (fn) =>     
    function curried(...args) {
        const haveEnoughArgs = args.length >= fn.length        
        const partiallyApplied = (...moreArgs) => 
            curried(...args.concat(moreArgs))   
             
        if (haveEnoughArgs) return fn(...args)        
        return partiallyApplied    
    }

Он возвращает функцию, которая может обрабатывать такие случаи:

  • карри (someFunction (1) (2) (3))
  • карри (someFunction (1, 2, 3))
  • карри (someFunction (1, 2) (3))
  • карри (someFunction (1) (2))

Что бы мы хотели добавить к этому списку?

А что, если мы хотим вызвать нашу каррированную функцию с меньшим / большим количеством аргументов, чем требуется?

Например, у нас есть функция, которая может бесконечно складывать числа:

const addInfinite = (...args) => args.reduce((acc, cur) => acc + cur, 0)

Мы хотим карри, а затем складывать числа по одному или несколько из некоторого бесконечного потока. Мы не знаем, когда у нас закончатся числа, которые нужно добавить, но мы знаем, что нам нужно только 10 из них. Допустим, мы пытаемся вычислить какой-то статистический показатель в режиме реального времени. Поэтому нам нужно обновить его как можно скорее, и 10 точек данных будет достаточно.

Прохладный. Давай попробуем. Мы карри и скармливаем ему наше первое число из потока:

curry(addInfinite)(5) // returns 5

Подождите, он вернул значение вместо частично примененной функции. Теперь мы не можем кормить его другими числами. Но почему?

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

curry(addInfinite)() // returns 0

Как это исправить?

Мы можем использовать каррирование до фиксированной арности!

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

А сейчас:

curryN(10, addInfinite)(5)(2)(8) // returns partially applied function
curryN(10, addInfinite)(5)(2)(8)(5)(2)(1)(8)(3)(4)(5) // returns 43

Отлично, мы получили то, что хотели!

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

По сути, классическое каррирование - это частный случай каррирования с фиксированной арностью.

А код классической арности у нас уже есть. Итак, просто немного изменим:

const curryN = (n, fn) =>    
    function curried(...args) {
        // compare to n instead of fn.length
        const haveEnoughArgs = args.length >= n      
        const partiallyApplied = (...moreArgs) => 
            curried(...args.concat(moreArgs))   
             
        if (haveEnoughArgs) return fn(...args)        
        return partiallyApplied    
    }

Вот и все! У нас есть новая функция, которая выполняет каррирование до фиксированной арности.

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

export const curry = (fn) => curryN(fn.length, fn)

Отлично!

Кнопка остановки

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

const addFourOrLessNumbers = (a, b, c, d) 
    => (a || 0) + (b || 0) + (c || 0) + (d || 0)

Он может складывать любое количество значений до 4. Если мы назовем это так

addFourOrLessNumbers(1, 2)

он вернет 3, как и ожидалось. JavaScript сделал свое чудо и заменил переменные «c» и «d» неопределенными значениями. Значит, рассчитано правильно. Но теперь давайте карри с помощью классической функции карри, а затем вызовем с теми же аргументами:

curry(addFourOrLessNumbers)(1, 2) // returns function

Ожидаем получить 3, но получили частично примененную функцию. Почему? Потому что каррированная функция ожидает всех своих аргументов перед тем, как ее можно будет выполнить.

Хорошо, как мы узнали выше, для этой цели мы можем использовать curryN.

curryN(2, addFourOrLessNumbers)(1, 2) // returns 3

Оно работает. Но что, если нам нужно вызвать эту функцию с 2, 3 или 4 в одном и том же месте в зависимости от некоторого условия. Конечно, мы можем сделать отдельную функцию для каждого такого случая с помощью curryN, но это не очень удобно, правда? И представьте себе аналогичную функцию, но с 10 или более аргументами.

const addTwoOrLessNumbers = curryN(2, addTenOrLessNumbers)
const addThreeOrLessNumbers = curryN(3, addTenOrLessNumbers)
const addFourOrLessNumbers = curryN(4, addTenOrLessNumbers)
...
if (someCondition && numbers.length === 2)addTwoOrLessNumbers(...)
else if (someCondition && numbers.length === 3)
    addThreeOrLessNumbers(...)
...

Не круто.

Так что классическое карри здесь не поможет, как и карриN. Что мы делаем?

Функциональный мир подвел нас? Должны ли мы просто принять это, сдаться суровой реальности, переосмыслить свою жизнь и двигаться дальше?

Нет! Потому что у нас есть вариативное каррирование! Итак, мы можем просто сделать это:

const curriedAddFourOrLessNumbers = curryV(addFourOrLessNumbers)
curriedAddFourOrLessNumbers(1)(3)
if (someCondition)curriedAddFourOrLessNumbers() // returns 4

Прохладный!

Каррирование с переменным числом аргументов - это каррирование, которое позволяет выполнять каррирование функции с переменным числом аргументов через завершение.

Завершение каррирования - это процесс вызова каррированной функции до того, как она получит все свои аргументы. Он работает, вызывая каррированную функцию с пустым аргументом.

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

Теперь напишем функцию вариативного каррирования:

const curryV = (fn) =>         
    function curried(...args) {        
        let shouldRunRightNow = args.length === 0        
        const haveEnoughArgs = args.length >= fn.length
        const partiallyApplied = (...moreArgs) => {
            shouldRunRightNow = moreArgs.length === 0
            if (shouldRunRightNow) return fn(...args)
            return curried(...args.concat(moreArgs))        
        }  
                  
        if (haveEnoughArgs || shouldRunRightNow) return fn(...args)
        return partiallyApplied        
    }

Как видите, он очень похож на классический карри, но более мощный.

Вариативное каррирование = классическое каррирование + возможность получать значение из каррированной функции по запросу (завершение).

Это очень полезно в случаях с вариативными функциями (функциями с разной степенью арности).

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

Если у вас есть какие-либо вопросы, не стесняйтесь оставлять здесь комментарии или связываться со мной через мой веб-сайт (ссылка в моем профиле Medium - ›« Как со мной связаться? »). Я с радостью вам помогу.

Вы можете просто использовать библиотеку

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

Я уже сделал это за тебя. Вот пакет npm и GitHub.

Это все!

Надеюсь, это путешествие было интересным.

Если у вас есть какие-либо вопросы, не стесняйтесь оставлять здесь комментарии или связываться со мной через мой веб-сайт (ссылка в моем профиле Medium - ›« Как со мной связаться? »). Я с радостью вам помогу.

Спасибо за чтение!