← JavaScript/Каррирование#100 из 383← ПредыдущийСледующий →+30 XP
Полезно по теме:Гайд: как учить JavaScriptПрактика: JS базаПрактика: async и сетьТермин: Closure

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

В Stripe SDK форматирование суммы зависит от валюты, а валюта известна при инициализации — до того как суммы станут известны. Каррирование позволяет «заморозить» первый аргумент и получить специализированную функцию: formatRUB = formatCurrency('RUB'), затем formatRUB(1500). Это мощный паттерн для создания переиспользуемых специализированных функций.

Какую проблему решает

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

На основе предыдущих уроков

  • Функции — функции как значения, аргументы
  • Стрелочные функции — краткий синтаксис для каррирования
  • Замыкания — замыкание сохраняет первый аргумент
  • Rest/Spread — ...args в универсальной curry()
  • Базовый пример

    // Обычная функция
    function add(a, b) { return a + b }
    add(3, 4)  // 7
    
    // Каррированная версия
    const addC = a => b => a + b
    
    addC(3)(4)   // 7
    
    // Создаём специализированные функции
    const add5 = addC(5)   // фиксируем первый аргумент
    add5(10)  // 15
    add5(20)  // 25

    Частичное применение — реальная польза

    const multiply = a => b => a * b
    const double = multiply(2)
    const triple = multiply(3)
    
    double(7)  // 14
    triple(7)  // 21
    
    [1, 2, 3, 4, 5].map(double)  // [2, 4, 6, 8, 10]
    [1, 2, 3, 4, 5].map(triple)  // [3, 6, 9, 12, 15]

    Реальные применения

    // Форматирование валюты — currency известна при инициализации
    const formatCurrency = currency => amount =>
      new Intl.NumberFormat('ru-RU', { style: 'currency', currency }).format(amount)
    
    const formatRUB = formatCurrency('RUB')
    const formatUSD = formatCurrency('USD')
    
    formatRUB(1500)   // '1 500,00 ₽'
    formatUSD(29.99)  // '29,99 $'
    
    const prices = [1000, 2500, 750]
    prices.map(formatRUB)  // ['1 000,00 ₽', '2 500,00 ₽', '750,00 ₽']
    
    // Логгер с preset-уровнем — уровень известен при настройке
    const log = level => message => console.log(`[${level}] ${message}`)
    const info  = log('INFO')
    const error = log('ERROR')
    const warn  = log('WARN')
    
    info('Сервер запущен')    // [INFO] Сервер запущен
    error('Нет соединения')  // [ERROR] Нет соединения
    
    // Валидатор с конфигурацией
    const minLength = min => str => str.length >= min
    const maxLength = max => str => str.length <= max
    const hasDigit  = () => str => /\d/.test(str)
    
    const isValidPassword = str =>
      minLength(8)(str) && maxLength(32)(str) && hasDigit()(str)
    
    isValidPassword('secret123')  // true
    isValidPassword('hi')         // false — слишком короткий
    isValidPassword('noDIGITS')   // false — нет цифры

    Универсальная функция curry()

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

    function curry(fn) {
      return function curried(...args) {
        if (args.length >= fn.length) {
          return fn.apply(this, args)  // аргументов достаточно — вызываем
        }
        return function(...moreArgs) {
          return curried.apply(this, args.concat(moreArgs))  // накапливаем
        }
      }
    }
    
    const sum = curry((a, b, c) => a + b + c)
    
    sum(1)(2)(3)   // 6
    sum(1, 2)(3)   // 6 — можно группировать
    sum(1)(2, 3)   // 6
    sum(1, 2, 3)   // 6 — как обычная функция

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

  • Каррирование: трансформирует в цепочку унарных функций f(a)(b)(c)
  • Частичное применение: фиксирует часть аргументов через bind или замыкание
  • // Частичное применение через bind
    const clamp = (min, max, value) => Math.min(Math.max(value, min), max)
    const clamp0to100 = clamp.bind(null, 0, 100)  // фиксируем min и max
    
    clamp0to100(50)   // 50
    clamp0to100(-10)  // 0
    clamp0to100(200)  // 100

    Типичные ошибки

    Ошибка 1: путаница с порядком аргументов

    // Неудачный порядок — конфигурация в конце, данные первыми
    const replace = (str, pattern, replacement) =>
      str.replace(pattern, replacement)
    
    // Не каррируется удобно — str меняется, а pattern обычно фиксирован
    
    // Удачный порядок — конфигурация первая, данные последними
    const replace2 = pattern => replacement => str =>
      str.replace(pattern, replacement)
    
    const removeTags = replace2(/<[^>]+>/g)('')  // фиксируем конфигурацию
    removeTags('<b>Hello</b>')  // 'Hello'
    removeTags('<p>World</p>')  // 'World'

    Ошибка 2: каррирование функций с rest-аргументами

    function curry(fn) {
      return function curried(...args) {
        if (args.length >= fn.length) return fn.apply(this, args)
        return (...more) => curried(...args, ...more)
      }
    }
    
    // fn.length не работает для rest-параметров
    const variadic = (...args) => args.reduce((a, b) => a + b, 0)
    console.log(variadic.length)  // 0 — fn.length игнорирует rest!
    
    // Для вариадических функций используй ручное каррирование

    В реальных проектах

  • Stripe/ЮKassa: createCharge(currency)(amount)(description)
  • React: connect(mapState)(mapDispatch)(Component) (Redux)
  • Ramda/lodash/fp: все функции автоматически каррированы
  • Логирование: log(level)(context)(message)
  • Примеры

    Каррированные функции для интернет-магазина: форматирование цен, фильтрация, логирование

    // 1. Форматирование цен с конфигурацией
    const formatCurrency = currency => amount =>
      new Intl.NumberFormat('ru-RU', { style: 'currency', currency }).format(amount)
    
    const formatRUB = formatCurrency('RUB')
    const formatUSD = formatCurrency('USD')
    
    const prices = [1000, 2500, 750]
    console.log(prices.map(formatRUB))
    // ['1 000,00 ₽', '2 500,00 ₽', '750,00 ₽']
    
    console.log(formatUSD(29.99))  // '29,99 $'
    
    // 2. Каррированный фильтр по полю объекта
    const byField = field => value => obj => obj[field] === value
    const isActive = byField('active')(true)
    const isAdmin  = byField('role')('admin')
    
    const users = [
      { name: 'Иван',  active: true,  role: 'user'  },
      { name: 'Анна',  active: true,  role: 'admin' },
      { name: 'Олег',  active: false, role: 'user'  },
    ]
    console.log(users.filter(isActive).map(u => u.name))  // ['Иван', 'Анна']
    console.log(users.filter(isAdmin).map(u => u.name))   // ['Анна']
    
    // 3. Каррированный логгер для разных компонентов
    const createLogger = level => context => message =>
      console.log(`[${level}] [${context}] ${message}`)
    
    const info  = createLogger('INFO')
    const error = createLogger('ERROR')
    
    const authLog  = info('Auth')
    const orderLog = info('Orders')
    const dbError  = error('Database')
    
    authLog('Пользователь вошёл')     // [INFO] [Auth] Пользователь вошёл
    orderLog('Заказ #101 создан')     // [INFO] [Orders] Заказ #101 создан
    dbError('Соединение потеряно')    // [ERROR] [Database] Соединение потеряно
    
    // 4. Универсальная curry()
    function curry(fn) {
      return function curried(...args) {
        if (args.length >= fn.length) return fn.apply(this, args)
        return (...moreArgs) => curried(...args, ...moreArgs)
      }
    }
    
    const clamp = curry((min, max, value) => Math.min(Math.max(value, min), max))
    const clamp0to100 = clamp(0, 100)
    
    console.log([50, -10, 200, 75].map(clamp0to100))  // [50, 0, 100, 75]

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

    В Stripe SDK форматирование суммы зависит от валюты, а валюта известна при инициализации — до того как суммы станут известны. Каррирование позволяет «заморозить» первый аргумент и получить специализированную функцию: formatRUB = formatCurrency('RUB'), затем formatRUB(1500). Это мощный паттерн для создания переиспользуемых специализированных функций.

    Какую проблему решает

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

    На основе предыдущих уроков

  • Функции — функции как значения, аргументы
  • Стрелочные функции — краткий синтаксис для каррирования
  • Замыкания — замыкание сохраняет первый аргумент
  • Rest/Spread — ...args в универсальной curry()
  • Базовый пример

    // Обычная функция
    function add(a, b) { return a + b }
    add(3, 4)  // 7
    
    // Каррированная версия
    const addC = a => b => a + b
    
    addC(3)(4)   // 7
    
    // Создаём специализированные функции
    const add5 = addC(5)   // фиксируем первый аргумент
    add5(10)  // 15
    add5(20)  // 25

    Частичное применение — реальная польза

    const multiply = a => b => a * b
    const double = multiply(2)
    const triple = multiply(3)
    
    double(7)  // 14
    triple(7)  // 21
    
    [1, 2, 3, 4, 5].map(double)  // [2, 4, 6, 8, 10]
    [1, 2, 3, 4, 5].map(triple)  // [3, 6, 9, 12, 15]

    Реальные применения

    // Форматирование валюты — currency известна при инициализации
    const formatCurrency = currency => amount =>
      new Intl.NumberFormat('ru-RU', { style: 'currency', currency }).format(amount)
    
    const formatRUB = formatCurrency('RUB')
    const formatUSD = formatCurrency('USD')
    
    formatRUB(1500)   // '1 500,00 ₽'
    formatUSD(29.99)  // '29,99 $'
    
    const prices = [1000, 2500, 750]
    prices.map(formatRUB)  // ['1 000,00 ₽', '2 500,00 ₽', '750,00 ₽']
    
    // Логгер с preset-уровнем — уровень известен при настройке
    const log = level => message => console.log(`[${level}] ${message}`)
    const info  = log('INFO')
    const error = log('ERROR')
    const warn  = log('WARN')
    
    info('Сервер запущен')    // [INFO] Сервер запущен
    error('Нет соединения')  // [ERROR] Нет соединения
    
    // Валидатор с конфигурацией
    const minLength = min => str => str.length >= min
    const maxLength = max => str => str.length <= max
    const hasDigit  = () => str => /\d/.test(str)
    
    const isValidPassword = str =>
      minLength(8)(str) && maxLength(32)(str) && hasDigit()(str)
    
    isValidPassword('secret123')  // true
    isValidPassword('hi')         // false — слишком короткий
    isValidPassword('noDIGITS')   // false — нет цифры

    Универсальная функция curry()

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

    function curry(fn) {
      return function curried(...args) {
        if (args.length >= fn.length) {
          return fn.apply(this, args)  // аргументов достаточно — вызываем
        }
        return function(...moreArgs) {
          return curried.apply(this, args.concat(moreArgs))  // накапливаем
        }
      }
    }
    
    const sum = curry((a, b, c) => a + b + c)
    
    sum(1)(2)(3)   // 6
    sum(1, 2)(3)   // 6 — можно группировать
    sum(1)(2, 3)   // 6
    sum(1, 2, 3)   // 6 — как обычная функция

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

  • Каррирование: трансформирует в цепочку унарных функций f(a)(b)(c)
  • Частичное применение: фиксирует часть аргументов через bind или замыкание
  • // Частичное применение через bind
    const clamp = (min, max, value) => Math.min(Math.max(value, min), max)
    const clamp0to100 = clamp.bind(null, 0, 100)  // фиксируем min и max
    
    clamp0to100(50)   // 50
    clamp0to100(-10)  // 0
    clamp0to100(200)  // 100

    Типичные ошибки

    Ошибка 1: путаница с порядком аргументов

    // Неудачный порядок — конфигурация в конце, данные первыми
    const replace = (str, pattern, replacement) =>
      str.replace(pattern, replacement)
    
    // Не каррируется удобно — str меняется, а pattern обычно фиксирован
    
    // Удачный порядок — конфигурация первая, данные последними
    const replace2 = pattern => replacement => str =>
      str.replace(pattern, replacement)
    
    const removeTags = replace2(/<[^>]+>/g)('')  // фиксируем конфигурацию
    removeTags('<b>Hello</b>')  // 'Hello'
    removeTags('<p>World</p>')  // 'World'

    Ошибка 2: каррирование функций с rest-аргументами

    function curry(fn) {
      return function curried(...args) {
        if (args.length >= fn.length) return fn.apply(this, args)
        return (...more) => curried(...args, ...more)
      }
    }
    
    // fn.length не работает для rest-параметров
    const variadic = (...args) => args.reduce((a, b) => a + b, 0)
    console.log(variadic.length)  // 0 — fn.length игнорирует rest!
    
    // Для вариадических функций используй ручное каррирование

    В реальных проектах

  • Stripe/ЮKassa: createCharge(currency)(amount)(description)
  • React: connect(mapState)(mapDispatch)(Component) (Redux)
  • Ramda/lodash/fp: все функции автоматически каррированы
  • Логирование: log(level)(context)(message)
  • Примеры

    Каррированные функции для интернет-магазина: форматирование цен, фильтрация, логирование

    // 1. Форматирование цен с конфигурацией
    const formatCurrency = currency => amount =>
      new Intl.NumberFormat('ru-RU', { style: 'currency', currency }).format(amount)
    
    const formatRUB = formatCurrency('RUB')
    const formatUSD = formatCurrency('USD')
    
    const prices = [1000, 2500, 750]
    console.log(prices.map(formatRUB))
    // ['1 000,00 ₽', '2 500,00 ₽', '750,00 ₽']
    
    console.log(formatUSD(29.99))  // '29,99 $'
    
    // 2. Каррированный фильтр по полю объекта
    const byField = field => value => obj => obj[field] === value
    const isActive = byField('active')(true)
    const isAdmin  = byField('role')('admin')
    
    const users = [
      { name: 'Иван',  active: true,  role: 'user'  },
      { name: 'Анна',  active: true,  role: 'admin' },
      { name: 'Олег',  active: false, role: 'user'  },
    ]
    console.log(users.filter(isActive).map(u => u.name))  // ['Иван', 'Анна']
    console.log(users.filter(isAdmin).map(u => u.name))   // ['Анна']
    
    // 3. Каррированный логгер для разных компонентов
    const createLogger = level => context => message =>
      console.log(`[${level}] [${context}] ${message}`)
    
    const info  = createLogger('INFO')
    const error = createLogger('ERROR')
    
    const authLog  = info('Auth')
    const orderLog = info('Orders')
    const dbError  = error('Database')
    
    authLog('Пользователь вошёл')     // [INFO] [Auth] Пользователь вошёл
    orderLog('Заказ #101 создан')     // [INFO] [Orders] Заказ #101 создан
    dbError('Соединение потеряно')    // [ERROR] [Database] Соединение потеряно
    
    // 4. Универсальная curry()
    function curry(fn) {
      return function curried(...args) {
        if (args.length >= fn.length) return fn.apply(this, args)
        return (...moreArgs) => curried(...args, ...moreArgs)
      }
    }
    
    const clamp = curry((min, max, value) => Math.min(Math.max(value, min), max))
    const clamp0to100 = clamp(0, 100)
    
    console.log([50, -10, 200, 75].map(clamp0to100))  // [50, 0, 100, 75]

    Задание

    Реализуй универсальную функцию curry(fn), которая каррирует функцию с любым числом аргументов. Используй её для создания набора утилит: каррированный clamp(min, max, value), pipe для двух функций и валидатор длины строки.

    Подсказка

    fn.apply(this, args) — вызов функции с накопленными аргументами. args.concat(moreArgs) — объединяем старые и новые аргументы. fn.length — количество параметров (не работает для rest-параметров). clamp(0)(100)(value) или clamp(0, 100)(value) — оба варианта должны работать.

    Загружаем среду выполнения...
    Загружаем AI-помощника...