Когда одни только чистые функции не работают

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

Разве чистые функции не должны возвращать одни и те же значения на основе своих входных данных?

Правильный.

Итак, что я имею в виду под контекстно-зависимыми чистыми функциями?

Возьмите этот очень простой пример:

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

Это действительно чистая функция. Если бы я использовал его, я бы сделал это:

add(1, 2)
// => 3

Но обратите внимание на контекст? Контекст, с которым я имею дело, — это числа. Вместо этого я мог бы использовать строки.

add('tom', 'harry')
// => tomharry

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

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

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

const add
  = (a, b) => {
    if (!Number.isInteger(a) || !Number.isInteger(b)) {
      return new Error('cannot add non-numbers')
    }
    return a + b
  }

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

Так в чем проблема?

Скорее всего, даже мои небольшие услуги не будут выполнять одну функцию. Это будут как минимум десятки функций, если не сотни.

JsDocs в помощь

Возьмем следующий пример.

/**
 * Adds two numbers together
 * @param {number} a 
 * @param {number} b
 */
const add = (a, b) => a + b

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

Пример контекста

Что, если у нас есть более сложные типы. Например, сложить возраст двух человек.

module.export.add = (a, b) => a.age + b.age

Мы можем использовать типы для обозначения контекста в javascript или классов в PHP.

/**
 * @param {Person} a
 * @param {Person} b
 */
module.export.add = (a, b) => a.age + b.age
----
public function add(Person $a, Person $b)
{
   return $a->age() + $b->age();
}

Как бы мы определили человека?

Используя jsDocs или DocBlocks, у нас будет что-то вроде этого:

/**
 * A person object with a name and age. 
 * @typedef {Object<string, any>} Person 
 * @property {string} name The name of the person. 
 * @property {number} age The age of the person. 
 */

Я благодарю этот пост за приведенный выше пример: https://medium.com/@trukrs/type-safe-javascript-with-jsdoc-7a2a63209b76

Обратите внимание, что и в примере с числом, и в примере с человеком я использую одно и то же имя функции, добавить. Меняется только контекст.

Охватывая контекст

Есть проблема.

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

Возьмем, к примеру, пользователь вводит свой возраст:

let personA = {
  age: 'Forty'
}
let personB = {
  age: 33
}

Мы не можем сложить эти два числа вместе, не преобразовав «сорок» в 40. Это подчеркивает, насколько важна проверка. Поэтому, прежде чем мы сможем доверять контексту нашего приложения, все нужно проверить. Неважно, получены ли данные из API, базы данных, файла или пользовательского ввода.

Вот очень простой пример (с отсутствующим кодом):

// User enters age
let input = {
  age: '40'
}
// Validate into datatype
let createPerson = (input) => {
  if (!isNumber(input.age)) {
    // throw or return error
  }
  return { age: ParseInt(input.age) }
}
let person = createPerson(input)
// Then we can do our contextual stuff knowing that our types are correct
module.export.add = (a, b) => a.age + b.age
let totalAge = add(person, otherPerson)

Вот и все. Шестиугольная архитектура и подход Скотта Влашина к разработке на основе типов помогли мне понять контекст приложения и то, как его использовать для упрощения кода.

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