Функциональное программирование - это здорово. С появлением React все больше и больше внешнего кода JavaScript пишется с учетом принципов FP. Но как нам начать использовать мышление FP в повседневном коде, который мы пишем? Я попытаюсь использовать обычный блок кода и шаг за шагом реорганизовать его.

Наша проблема: пользователь, который заходит на нашу /login страницу, может иметь redirect_to параметр запроса. Вроде /login?redirect_to=%2Fmy-page. Обратите внимание, что %2Fmy-page на самом деле /my-page, когда он закодирован как часть URL. Нам нужно извлечь эту строку запроса и сохранить ее в локальном хранилище, чтобы после входа в систему пользователь мог быть перенаправлен на my-page.

Шаг no 0: императивный подход

Если бы нам пришлось выразить решение в простейшей форме выдачи списка команд, как бы мы его записали? Нам нужно будет

  1. Разберите строку запроса.
  2. Получите значение redirect_to.
  3. Расшифруйте это значение.
  4. Сохраните декодированное значение в localStorage.

И мы также должны поместить блоки try catch вокруг «небезопасных» функций. При этом наш блок кода будет выглядеть так:

Шаг # 1: Записываем каждый шаг как функцию

На мгновение забудем о блоках try catch и попробуем выразить здесь все как функцию.

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

Раньше мы бы тестировали основную функцию в целом. Но теперь у нас есть 4 функции меньшего размера, и некоторые из них просто проксируют другие функции, поэтому площадь, которую необходимо протестировать, намного меньше.

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

Шаг # 2: попытка составить функции

Хорошо. Теперь кажется, что функция persistRedirectToParams - это «композиция» из 4 других функций. Давайте посмотрим, сможем ли мы записать эту функцию как композицию, исключив тем самым промежуточные результаты, которые мы сохраняем как consts.

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

Шаг # 3: более читаемая композиция

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

С помощью compose наш код будет выглядеть так:

Одна вещь с compose заключается в том, что он сокращает функции справа налево. Итак, первая функция, которая вызывается в цепочке compose, является последней функцией.

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

Шаг # 4: обвязка и сплющивание

К счастью, есть pipe. pipe делает то же самое, что и compose, но в обратном порядке. Итак, первая функция в цепочке - это первая функция, обрабатывающая результат.

Кроме того, кажется, что наша persistRedirectToParams функция стала оболочкой для другой функции, которую мы вызываем op. Другими словами, все, что он делает, это выполняет op. Мы можем избавиться от оболочки и «сгладить» нашу функцию.

Почти готово. Помните, что мы оставили наш блок try-catch позади, чтобы привести его в текущее состояние? Что ж, нам нужен способ вернуть это. qs.parse небезопасно, как и storeRedirectToQuery. Один из вариантов - сделать их функциями-оболочками и поместить их в try-catch блоки. Другой, функциональный способ - выразить try-catch как функцию.

Шаг # 5: обработка исключений как функция

Есть несколько утилит, которые это делают, но давайте попробуем написать что-нибудь сами.

Наша функция здесь ожидает объект opts, который будет содержать функции tryer и catcher. Он вернет функцию, которая при вызове с аргументами вызывает tryer с указанными аргументами, а в случае неудачи вызывает catcher. Теперь, когда у нас есть небезопасные операции, мы можем поместить их в раздел tryer и, если они не удастся, спасти и дать безопасный результат из раздела ловушки (и даже зарегистрировать ошибку).

Шаг # 6: Собираем все вместе

Итак, с учетом этого, наш окончательный код выглядит так:

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

Теперь у нас есть реализация гораздо более крупной функции, состоящей из 4 отдельных функций, которые очень связаны, слабо связаны, могут быть протестированы независимо, могут быть повторно использованы независимо, учитывают сценарии исключений и являются высоко декларативными. . (И, IMO, их читать немного приятнее.)

Есть некоторый синтаксический сахар FP, который делает это еще лучше, но это на другой день.

Обновление №1. В продолжение этой истории я немного расширил синтаксический сахар FP, о котором говорил. Вы можете найти это здесь: If-else и try-catch как функциональные конструкции.