Принцип разделения интересов с использованием каррирования!

Эта статья является частью серии статей о функциональном программировании

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

Оглавление

  • Что такое каррирование
  • Как работает каррирование
  • Почему каррирование
  • Почему каррирование делает наш код лучше
  • Замыкание и каррирующие отношения
  • Дополнительные примеры
  • Заключение

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

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

Как работает каррирование

Допустим, у нас есть функция add

const add = (a, b) => a + b

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

const add = (a) => (b) => a + b

Где это можно использовать так:

const addOne = add(1) // addOne = (b) => 1 + b

Но давайте представим, что у нас есть функция curry, которая принимает функцию и каррирует ее. Например:

const add = curry((a, b) => a + b)

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

const addOne = add(1) // addOne = (b) => 1 + b

Итак, сначала мы создали addOne, передав 1 в качестве первого параметра (a) каррированной функции add. Что дало другую функцию, которая ожидает остальные параметры, где логика add не будет выполняться, пока не будут предоставлены все параметры.

addOne(2) // 3

Теперь, передавая 2 (как b) в addOne; выполняет логику 1 + 2

Быстрый вывод:

curry принимает функцию и делает ее параметры ленивыми, другими словами, вы предоставляете эти параметры по мере необходимости. Так же, как addOne

Небольшое примечание:

Вы по-прежнему можете вызывать каррированную версию функции add следующим образом:

const three = add(1, 2)

Таким образом, он либо принимает аргументы по частям, либо все аргументы сразу.

Почему каррирование

Каррирование сделает наш код:

  1. Очиститель
  2. Менее повторяющаяся передача параметров и менее подробный код
  3. Более компонуемый
  4. Более многоразовый

Почему каррирование делает наш код лучше

В основном, некоторые функции принимают данные «config» в качестве входных данных.

Если у нас есть функции, которые принимают параметры «config», нам лучше их каррировать, потому что эти «configs», вероятно, будут повторяться снова и снова.

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

const translator = (locale, text) => {/*translation*/}

Использование будет выглядеть так:

translator('fr', 'Hello')
translator('fr', 'Goodbye')
translator('fr', 'How are you?')

Каждый раз, когда мы вызываем translator, мы должны предоставлять locale и text. Что является избыточным и грязным для предоставления locale при каждом вызове.

Но вместо этого давайте карри translator вот так:

const translator = curry((locale, text) => {/*translation*/})
const inFrench = translator('fr') 

Теперь inFrench имеет fr as locale для функции curried translator и ждет предоставления text. Мы можем использовать это так:

inFrench('Hello')
inFrench('Goodbye')
inFrench('How are you?')

Каррирование действительно оказало нам большую услугу, нам не нужно указывать locale каждый раз, вместо этого каррирование inFrench имеет locale из-за каррирования.

После каррирования - в этом конкретном примере. Код:

  1. Очиститель
  2. Менее многословный и менее избыточный

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

В реальной жизни

На практике у нас есть динамический locale (у каждого пользователя свой язык) может быть fr, en, de или что-то еще. Так что вместо этого лучше переименовать inFrench в translate, где translate можно загрузить с любым locale.

Теперь у нас есть translator, который принимает locale в качестве «конфигурации» и text в качестве данных. Благодаря тому, что translator является каррированным, мы смогли отделить параметры конфигурации от параметров данных.

Зачем отделять «конфигурацию» от параметров «данные»?

Многие компоненты и функции нуждаются в использовании некоторых функций (в нашем случае translate), но не должны или не могут знать о части конфигурации(locale). Где эти компоненты или функции имеют только часть «данные»(text). Таким образом, эти функции смогут использовать эту функцию без необходимости знать о части конфигурации.

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

Когда мы применим эту идею

Когда мы знаем, что в функции есть «конфиг» и есть «данные», лучше их каррировать.

Каррирование даст нам возможность разделять их. И это признак зрелого дизайна системы. Потому что одним из главных столпов качества кода является разделение задач.

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

Замыкание и каррирующие отношения

Закрытие: функция, возвращаемая "родительской" функцией и имеющая доступ к внутреннему состоянию родительской функции. (описано ранее здесь)

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

Другие примеры

Прежде чем мы погрузимся глубже

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

Прототип массива имеет такие утилиты, как filter, map и другие. Но они не поддерживают карри, потому что используют запись через точку (.).

Итак, давайте конвертируем их в карри-формат:

const filter     = (fn, list)   => list.filter(fn)
const map        = (fn, list)   => list.map(fn)
const startsWith = (starter, s) => s.startsWith(starter)

Теперь мы можем использовать их так:

const lessThan18 = user => user.age < 18
// Converting this format
const filteredUsers = users.filter(lessThan18)
// To this format instead
const filteredUsers = filter(lessThan18, users)

(Мы исключили запись через точку и передали обработанные данные в качестве последнего параметра)

Затем мы curry их. Где эта curry функция будет принимать функцию и возвращать каррированную функцию (вы можете найти реализацию здесь):

const filter     = curry((fn, list)   => list.filter(fn))
const map        = curry((fn, list)   => list.map(fn))
const startsWith = curry((starter, s) => s.startsWith(starter))

Примеры

Теперь мы можем привести несколько осмысленных примеров…

Пример 1️:

Дан список чисел, увеличить все числа на 1

Ввод: [1, 2, 3, 4, 5]

Вывод: [2, 3, 4, 5, 6]

Решение:

// the curried `add` function that we defined earlier
const addOne = add(1)
const incrementNumbers = map(addOne)
const incrementedNumbers = incrementNumbers(numbers)

Пример 2️:

Учитывая строку, сохранить все слова, начинающиеся с буквы «C».

Ввод: "currying is awesome”

Вывод: “currying”

Решение:

const startsWithC = startsWith('c')
const filterStartsWithC = filter(startsWithC)
const filteredWords = filterStartsWithC(words)

Пример 3️:

Дан список диапазонов и список чисел; Создайте массив функций, которые могут фильтровать числа на основе предоставленных диапазонов.

Ввод:

const ranges = [
  {min: 10, max: 100}, 
  {min: 100, max: 500}, 
  {min: 500, max: 999}
]
const numbers = [30, 50, 110, 200, 650, 700, 1000]
// 30 & 50 within 1st range
// 110 & 200 within 2nd range
// 650 & 700 within 3rd range
// 1000 isn't in any range

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

Решение:

const isInRange = curry(
  (range, val) => val > range.min && val < range.max
)
const filters = ranges.map((range) => filter(isInRange(range)))

В этом примере есть двойное карри, если вы его заметили, filter и isInRange

filters теперь представляет собой список функций, каждая из которых ожидает обработки numbers, загружается (в редакции "config") с min и max

Пояснение:

Лучшим объяснением было бы развернуть каррирование и вместо этого использовать обычные функции…

const isInRange = (range, val) => val > range.min && val < range.max
const filters = ranges.map(
  (range) => (numbers) => numbers.filter(
    number => isInRange(range, number)
  )
)

Помните, что () => () => ... — это еще одна реализация каррирования. Простая версия каррирования.

И все благодаря карри! ❤️

Заключение

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

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

Большое спасибо, что нашли время прочитать эту статью ❤️ Я готовлю следующие в этой серии. Пожалуйста, дайте мне знать, что вы думаете об этой статье или серии в комментариях.

Это статья из серии статей о функциональном программировании.

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

В этой серии обсуждаются:

0. Краткое сравнение парадигм программирования

  1. Первоклассные функции
  2. Чистые функции
  3. Каррирование (эта статья)
  4. "Состав"
  5. Функторы
  6. Монады