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

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

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

Оплата счетов состоит из нескольких подшагов.

  • Подпишите счет
  • Передайте счет почтовой даме
  • Заплати за это
  • Возьмите конформационную бумагу

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

function signTheBill(bill) {
    if (isPresent(bill)) {
        return sign(bill);
    } else {
        throw new Error('bill is missing');
    }
}

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

Поскольку шаг «Передать счет» не имеет смысла, если счета нет, вместо продолжения вы выдаете ошибку, которая останавливает выполнение.

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

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

Вероятно, лучше было бы сказать «к черту», ​​отбросить все шаги, связанные с выставлением счетов, и перейти к другим задачам.

Именно в этом разница между ошибкой и неудачным вычислением. Но что это за две вещи?

Ошибка

Старая добрая ошибка — это всего лишь способ, как компьютер решает неразрешимую задачу. Программа перестает работать и остается такой, если только она не перезапущена каким-либо родительским процессом или человеком.

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

Неудачное вычисление

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

Каждая дорога — это функция, обычно связанная с другой функцией. Никаких инструкций, только одно гигантское выражение, ожидающее оценки («ожидание, когда вас отвезут на машине»).

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

Что бы сделал Иисус?

Какой из этих двух подходов выбрал бы Иисус? Вот в чем вопрос. И это не так риторически, как звучит. Иисус уже сделал свой выбор.

Чтобы понять Иисуса, давайте взглянем на абсолютный язык программирования Бога и на то, как в нем обрабатываются сбои. Что это за язык, спросите вы? Ну математика, понятно.

Давайте на минутку поговорим о нашем господине и спасителе алгебре.

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

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

Очередной призрак из теории категорий

Категория определяется объектами (в нашем простом примере числами) и стрелками (они же морфизмы, которые определяют переход от объекта A к объекту B. В основном функциями).

Формула A -> B в примере как 8 (*2) 16. (*2) — это стрелка/функция/операция, умножающая на два.

Как категория чисел с соответствующими стрелками умножения справляется с отсутствием значения?

Что ж, ноль распространяется дальше, и результирующее выражение оценивается как ноль.

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

Безмолвное лечение

Осторожно! Я не призываю вас позволять неудачам происходить молча. Конечно, у вас должен быть какой-то механизм для отслеживания ваших неудач. На самом деле не имеет значения, хотите ли вы логировать свои сбои в консоли, отправлять запрос в какой-нибудь сервис отслеживания ошибок или писать в какой-то лог-файл. Для нас это просто какой-то побочный эффект.

Мы знаем паттерн Функтора fmap, и поэтому вопрос «где должны проявляться побочные эффекты» для нас достаточно тривиален. Они должны быть спрятаны в композиции таким образом, чтобы ни одна стрелка/функция не была непосредственно связана с ними.

const f = g = h = x => x + 1
const compose = (...fns) => value =>
    fns.reduce((acc, fn) => acc ? fn(acc) : acc, value)
const pipeline = compose(f, g, h)
pipeline(4) // 7

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

Заключение

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

Но, учитывая, что вы пытаетесь стать функциональным, именно так вы справляетесь с неудачами. Весь принцип становится еще веселее, если вы украшаете свои дороги дорожными знаками, например, шрифтовыми аннотациями.