← Курс/Функциональное программирование в JS#134 из 257+40 XP

Функциональное программирование в JS

Краткий ответ

Функциональное программирование (FP) — это стиль, при котором программа строится из чистых функций без побочных эффектов, данные не мутируются, а функции — объекты первого класса. В JS FP нативен: функции можно передавать как аргументы, возвращать из функций, хранить в переменных. Ключевые инструменты: map/filter/reduce, composе/pipe, каррирование, частичное применение.

Полный разбор

Чистые функции

Чистая функция — детерминирована (одинаковый ввод = одинаковый вывод) и не имеет побочных эффектов:

// НЕ чистая — читает внешнее состояние, имеет побочный эффект
let tax = 0.2
function calculatePrice(price) {
  console.log('Расчёт...')  // побочный эффект
  return price * (1 + tax)  // зависит от внешней переменной
}

// Чистая — предсказуема, изолирована
function calculatePrice(price, taxRate) {
  return price * (1 + taxRate)
}
// calculatePrice(100, 0.2) всегда вернёт 120

Иммутабельность

Не мутируй входные данные — возвращай новые:

// ПЛОХО — мутация аргумента
function addUser(users, user) {
  users.push(user)  // мутируем оригинальный массив!
  return users
}

// ХОРОШО — возвращаем новый массив
function addUser(users, user) {
  return [...users, user]
}

// Обновление объекта без мутации
function updateAge(user, newAge) {
  return { ...user, age: newAge }
}

Функции высшего порядка

Функция, принимающая или возвращающая другую функцию:

// map, filter, reduce — встроенные функции высшего порядка
const numbers = [1, 2, 3, 4, 5]

const doubled = numbers.map(x => x * 2)           // [2, 4, 6, 8, 10]
const evens = numbers.filter(x => x % 2 === 0)    // [2, 4]
const sum = numbers.reduce((acc, x) => acc + x, 0) // 15

// Собственная функция высшего порядка
function repeat(n, fn) {
  return Array.from({ length: n }, (_, i) => fn(i))
}
repeat(3, i => i * i)  // [0, 1, 4]

Композиция функций

// compose: выполняет функции справа налево (математическая нотация)
// compose(f, g, h)(x) === f(g(h(x)))
function compose(...fns) {
  return (x) => fns.reduceRight((acc, fn) => fn(acc), x)
}

// pipe: выполняет функции слева направо (читается как поток данных)
// pipe(f, g, h)(x) === h(g(f(x)))
function pipe(...fns) {
  return (x) => fns.reduce((acc, fn) => fn(acc), x)
}

// Пример использования
const trim = s => s.trim()
const toLowerCase = s => s.toLowerCase()
const addExclamation = s => s + '!'

const process = pipe(trim, toLowerCase, addExclamation)
process('  Hello World  ')  // 'hello world!'

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

// Каррирование: функция с N аргументами → цепочка функций с 1 аргументом
function curry(fn) {
  return function curried(...args) {
    if (args.length >= fn.length) {
      return fn.apply(this, args)
    }
    return function(...args2) {
      return curried.apply(this, args.concat(args2))
    }
  }
}

const add = curry((a, b, c) => a + b + c)
add(1)(2)(3)   // 6
add(1, 2)(3)   // 6
add(1)(2, 3)   // 6

// Частичное применение: фиксируем часть аргументов
function partial(fn, ...presetArgs) {
  return function(...laterArgs) {
    return fn(...presetArgs, ...laterArgs)
  }
}

const multiply = (a, b) => a * b
const double = partial(multiply, 2)
double(5)   // 10
double(10)  // 20

Императивный vs функциональный стиль

const orders = [
  { id: 1, amount: 150, status: 'completed', userId: 'u1' },
  { id: 2, amount: 80,  status: 'pending',   userId: 'u2' },
  { id: 3, amount: 300, status: 'completed', userId: 'u1' },
  { id: 4, amount: 50,  status: 'cancelled', userId: 'u3' },
]

// Императивно: получить сумму завершённых заказов пользователя u1
function getTotalImperative(orders, userId) {
  let total = 0
  for (let i = 0; i < orders.length; i++) {
    if (orders[i].userId === userId && orders[i].status === 'completed') {
      total += orders[i].amount
    }
  }
  return total
}

// Функционально: декларативно, читается как описание задачи
const getTotalFunctional = (orders, userId) =>
  orders
    .filter(o => o.userId === userId && o.status === 'completed')
    .map(o => o.amount)
    .reduce((sum, amount) => sum + amount, 0)

console.log(getTotalImperative(orders, 'u1'))   // 450
console.log(getTotalFunctional(orders, 'u1'))   // 450

Почему FP улучшает тестируемость

Чистые функции тестируются без моков и setup:

// Нечистая: требует mocking базы данных
async function getUser(id) {
  return await db.query('SELECT * FROM users WHERE id = ?', [id])
}

// Чистая логика легко тестируется
function formatUser(rawUser) {
  return {
    id: rawUser.id,
    name: rawUser.first_name + ' ' + rawUser.last_name,
    email: rawUser.email.toLowerCase()
  }
}
// test: formatUser({ id: 1, first_name: 'Ivan', last_name: 'Ivanov', email: 'Ivan@Example.COM' })
// ожидаем: { id: 1, name: 'Ivan Ivanov', email: 'ivan@example.com' }

Связанные уроки курса

  • Функции — основа FP, первоклассные объекты в JS
  • Методы массивов — map, filter, reduce как функции высшего порядка
  • Каррирование — детальный разбор с реализацией curry()
  • Как отвечать на собеседовании

    Начни с определений: «чистая функция», «иммутабельность», «функции первого класса». Покажи compose/pipe — это любимый вопрос на JS-собеседованиях. Объясни разницу между каррированием (f(a)(b)(c)) и частичным применением (partial(f, a)(b, c)). Подчеркни практическую ценность: FP-код легче тестировать, переиспользовать и рассуждать о нём.

    Красные флаги ответа

  • Путаница между каррированием и частичным применением — это разные вещи, хотя оба фиксируют аргументы
  • Мутация входных данных в «функциональных» примерах — нарушает принцип иммутабельности и делает код непредсказуемым
  • Незнание compose/pipe — базовые инструменты FP, без них нельзя строить функциональные пайплайны
  • Примеры

    Compose, pipe, partial application, curry и рефакторинг императивного кода в функциональный стиль

    // ===== COMPOSE И PIPE =====
    console.log('=== compose / pipe ===')
    
    function compose(...fns) {
      return (x) => fns.reduceRight((acc, fn) => fn(acc), x)
    }
    
    function pipe(...fns) {
      return (x) => fns.reduce((acc, fn) => fn(acc), x)
    }
    
    // Строковые трансформации
    const trim        = s => s.trim()
    const toLowerCase = s => s.toLowerCase()
    const capitalize  = s => s.charAt(0).toUpperCase() + s.slice(1)
    const addDot      = s => s.endsWith('.') ? s : s + '.'
    
    // pipe читается слева направо как поток
    const normalizeText = pipe(trim, toLowerCase, capitalize, addDot)
    console.log(normalizeText('  привет МИР  '))  // 'Привет мир.'
    console.log(normalizeText('HELLO'))            // 'Hello.'
    
    // compose читается справа налево (математически)
    const normalizeCompose = compose(addDot, capitalize, toLowerCase, trim)
    console.log(normalizeCompose('  HELLO  '))    // 'Hello.'
    
    // ===== ЧАСТИЧНОЕ ПРИМЕНЕНИЕ =====
    console.log('\n=== Частичное применение ===')
    
    function partial(fn, ...presetArgs) {
      return function(...laterArgs) {
        return fn(...presetArgs, ...laterArgs)
      }
    }
    
    // Базовые функции
    const multiply = (a, b) => a * b
    const add      = (a, b) => a + b
    const pow      = (base, exp) => Math.pow(base, exp)
    
    // Специализированные функции через partial
    const double   = partial(multiply, 2)
    const triple   = partial(multiply, 3)
    const add10    = partial(add, 10)
    const square   = partial(pow, undefined) // не подходит — нужен другой подход
    
    console.log(double(5))   // 10
    console.log(triple(5))   // 15
    console.log(add10(25))   // 35
    
    // Практичный пример: partial для fetch
    function fetchData(baseUrl, endpoint) {
      return `GET ${baseUrl}${endpoint}`  // упрощённо
    }
    
    const fetchFromAPI = partial(fetchData, 'https://api.example.com')
    console.log(fetchFromAPI('/users'))      // GET https://api.example.com/users
    console.log(fetchFromAPI('/products'))   // GET https://api.example.com/products
    
    // ===== КАРРИРОВАНИЕ =====
    console.log('\n=== Каррирование ===')
    
    function curry(fn) {
      return function curried(...args) {
        if (args.length >= fn.length) {
          return fn.apply(this, args)
        }
        return function(...args2) {
          return curried.apply(this, args.concat(args2))
        }
      }
    }
    
    const curriedAdd = curry((a, b, c) => a + b + c)
    console.log(curriedAdd(1)(2)(3))   // 6
    console.log(curriedAdd(1, 2)(3))   // 6
    console.log(curriedAdd(1)(2, 3))   // 6
    console.log(curriedAdd(1, 2, 3))   // 6
    
    // Практичный пример: каррированный filter
    const curriedFilter = curry((predicate, arr) => arr.filter(predicate))
    const isEven = x => x % 2 === 0
    const isPositive = x => x > 0
    
    const filterEvens    = curriedFilter(isEven)
    const filterPositive = curriedFilter(isPositive)
    
    console.log(filterEvens([1, 2, 3, 4, 5]))      // [2, 4]
    console.log(filterPositive([-2, -1, 0, 1, 2])) // [1, 2]
    
    // ===== ИМПЕРАТИВНЫЙ VS ФУНКЦИОНАЛЬНЫЙ =====
    console.log('\n=== Рефакторинг: императивный → функциональный ===')
    
    const employees = [
      { name: 'Алиса', dept: 'engineering', salary: 120000, senior: true },
      { name: 'Боб',   dept: 'marketing',   salary: 80000,  senior: false },
      { name: 'Карла', dept: 'engineering', salary: 150000, senior: true },
      { name: 'Денис', dept: 'engineering', salary: 95000,  senior: false },
      { name: 'Ева',   dept: 'marketing',   salary: 90000,  senior: true },
    ]
    
    // Императивно: средняя зарплата senior-инженеров
    function avgSeniorEngineerSalaryImperative(employees) {
      let total = 0
      let count = 0
      for (let i = 0; i < employees.length; i++) {
        const e = employees[i]
        if (e.dept === 'engineering' && e.senior) {
          total += e.salary
          count++
        }
      }
      return count > 0 ? total / count : 0
    }
    
    // Функционально: декларативно, без промежуточных переменных
    const avgSeniorEngineerSalaryFP = (employees) => {
      const salaries = employees
        .filter(e => e.dept === 'engineering' && e.senior)
        .map(e => e.salary)
    
      return salaries.length > 0
        ? salaries.reduce((sum, s) => sum + s, 0) / salaries.length
        : 0
    }
    
    const imp = avgSeniorEngineerSalaryImperative(employees)
    const fp  = avgSeniorEngineerSalaryFP(employees)
    console.log('Императивно:', imp)   // 135000
    console.log('Функционально:', fp)  // 135000