Как создать рандомное целое число в JavaScript

2 года назад·1 мин. на чтение

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

Эта функция выводит рандомное целое число из диапазона. Первое число диапазона включается, конечное - не включается. min и max - целые числа.
// [min, max)
const getRandomInt = (min, max) => {
  return Math.floor(Math.random() * (max - min) + min);
};
Например, если требуется получить рандомное число 1,2,3,...100, то вызов будет выглядеть таким образом:
console.log(getRandomInt(1, 101)) // 5, 31, 17, 89, ...

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

2 года назад·5 мин. на чтение

Композиция — это способ построения больших модулей из более мелких. В этой статье подробно рассмотрим композицию на примере JavaScript.

Это серия статей о функциональном программировании:
  1. Парадигмы программирования
  2. Композиция (рассматривается в этой статье)
  3. Функторы
  4. Каррирование
  5. Чистые функции
  6. Функции первого класса

Что такое композиция функций?

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

Как работает композиция?

Математическая запись определения композиции выглядит следующим образом:
(f ∘ g)(x) = f(g(x))
В JavaScript композиция, где f и g - это функции, будет выглядеть так:
const compose = (f, g) => (x) => f(g(x))
Функция compose является композицией функций f и g. Выходные данные функции g будут переданы на вход функции f. По другому эта запись выглядела бы следующим образом:
const compose = (f, g) => {
  return (x) => {
    const gResult = g(x)
    const fResult = f(gResult)
    return fResult
  }
}

Пример работы функции compose

Рассмотрим пример с более конкретными функциями:
const getAge = (user) => user.age
const isAgeAllowed = (age) => age >= 30
Теперь составим композицию из этих маленьких функций - getAge и isAgeAllowed:
const user = { name: 'John', age: 35 }
const isAllowedUser = compose(
 isAgeAllowed,
 getAge
)
isAllowedUser(user) // true
compose выполняет функции справа налево. Мы отправляем объект user в функцию isAgeAllowed. Далее user попадает сначала в getAge, потом результат этой функции попадает в isAgeAllowed.

compose и pipe

Функция pipe очень похожа на функцию compose. Они выполняют одну и ту же роль. Обе объединяют функции в цепочки. Однако их реализация и порядок выполнения функций отличается.

Реализация compose и pipe

compose реализован следующим образом:
const compose = (...fns) => {
  return (x) => fns.reduceRight((acc, fn) => fn(acc), x)
}
pipe реализован следующим образом:
const pipe = (...fns) => {
   return (x) => fns.reduce((acc, fn) => fn(acc), x)
}
Они отличаются лишь в применении функции reduce и reduceRight. Это влияет лишь на порядок выполнения функций.

Порядок выполнения функций

compose выполняет функции справа налево. pipe выполняет функции слева направо.

Пример

Предположим имеется 3 функции: f, g и h. При использовании compose:
compose(f, g, h)
        ←  ←  ←
Порядок выполнения функций будет таким h, g и далее f (справа налево). И выходные данные будут передаваться в следующую функцию. При использовании pipe:
pipe(f, g, h)
     →  →  →
Порядок выполнения функций будет таким f, g и далее h (слева направо). И выходные данные будут передаваться в следующую функцию.

Какую функцию использовать?

compose и pipe не сильно отличаются друг от друга. Они решают одну и ту же задачу. compose ближе к математической нотации (f ∘ g)(x) = f(g(x)). С pipe визуально легче воспринимать порядок выполнения функций.

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

В следующих примерах будем использовать pipe. Предположим, нужно сделать простой калькулятор цен, в котором нужно применить:
  • налог (30%, по умолчанию),
  • сервисный сбор (10у.е., по умолчанию),
  • скидку,
  • купон,
  • цену доставки на основе веса.
API калькулятора цен на основе этих требований будет выглядеть следующим образом:
const priceCalculator = (
  taxPercentage = 0.3,
  serviceFees = 10,
  price,
  discount,
  percentCoupon,
  weight,
  shippingPricePerKg
) => {
  // реализация
}

Реализация без композиции

const priceCalculator = (
  taxPercentage = 0.3,
  serviceFees = 10,
  price,
  discount,
  percentCoupon,
  weight,
  shippingPricePerKg
) => {
  return (
    price
     - (price * percentCoupon)
     - discount
     + (weight * shippingPricePerKg)
     + serviceFees
  ) * (1 + taxPercentage)
}
Эта функция выполняет свою задачу, но ее сложно читать, тестировать и отлаживать.

Реализация с композицией

const priceCalculator = (
  taxPercentage = 0.3,
  serviceFees = 10,
  price,
  discount,
  percentCoupon,
  weight,
  shippingPricePerKg
) => {
 const applyTax = (val) => val * (1 + taxPercentage)
 const applyServiceFees = (val) => val + serviceFees
 const applyPercentCoupon = (val) => val - val * percentCoupon
 const applyDiscount = (val) => val - discount
 const applyShippingCost = (val) => val + weight * shippingPricePerKg

 return pipe(
   applyPercentCoupon,
   applyDiscount,
   applyShippingCost,
   applyServiceFees,
   applyTax
 )(price)
}
Эта реализация выглядит более чистой, ее легко тестировать и отлаживать. Все это благодаря модульности.

Запуск примера

Запустим, передав только price. Очевидно, получим неверный результат, т.к. передали не все обязательные параметры.
priceCalculator(10) // NaN
Сначала продебажим. Для дебага pipe и compose можно добавить удобную функцию inspect. Она просто логирует в консоль входные данные и возвращает эти данные без изменений.
const inspect = (label) => (x) => {
  console.log(`${label}: ${x}`)
  return x
}
Добавим inspect в цепочку выполнения.
const priceCalculator = (
  taxPercentage = 0.3,
  serviceFees = 10,
  price,
  discount,
  percentCoupon,
  weight,
  shippingPricePerKg
) => {
  const applyTax = (val) => val * (1 + taxPercentage)
  const applyServiceFees = (val) => val + serviceFees
  const applyPercentCoupon = (val) => val - val * percentCoupon
  const applyDiscount = (val) => val - discount
  const applyShippingCost = (val) => val + weight * shippingPricePerKg

  return pipe(
    inspect('price'),
    applyPercentCoupon,
    inspect('after applyPercentCoupon'),
    applyDiscount,
    inspect('after applyDiscount'),
    applyShippingCost,
    inspect('after applyShippingCost'),
    applyServiceFees,
    inspect('after applyServiceFees'),
    applyTax
  )(price)
}
Результат будет примерно таким:
priceCalculator(10)
// price: undefined
// after applyPercentCoupon: NaN
// ...
Видим, price - undefined. Это произошло потому что price - третий аргумент, а мы передаем его первым.

Быстрый фикс

Сделаем так, чтобы функция принимала один объект:
const priceCalculator = ({
  taxPercentage = 0.3,
  serviceFees = 10,
  price,
  discount,
  percentCoupon,
  weight,
  shippingPricePerKg
}) => {
  // ...
}
Далее, используем:
priceCalculator({ price: 10 })
 
// price: 10
// after applyPercentCoupon: NaN
// ...
Все еще получаем NaN, в этот раз потому что не был передан percentCoupon и он имеет значение undefined.
Добавим для всех параметров, кроме price, значения по умолчанию.
const priceCalculator = ({
  taxPercentage = 0.3,
  serviceFees = 10,
  price,
  discount = 0,
  percentCoupon = 0,
  weight = 0,
  shippingPricePerKg = 0
}) => {
  // ...
}
Если запустить заново, получим результат:
priceCalculator({ price: 10 }) // 26
Это пример того как композиция позволяет нам тестировать и исправлять код проще и быстрее, просто проверяя области, которые вызывают подозрения. Проблема, которую мы отлаживали, была очень простой. Когда мы переходим к более масштабным модулям, все становится еще более трудным для проверки. Большие атомарные функции трудно поддерживать. Разделение функций на более мелкие упрощает отладку, тестирование, поддержку и разработку функций.

Для чего нужна композиция?

Композиция это объединение меньших модулей в более крупные. Если мы думаем оперируем модулями (что обеспечивается композицией), мы улучшаем:
  • модульное мышление,
  • тестируемость,
  • возможность отладки,
  • поддерживаемость.

Итоги

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

Также будет интересно: