Функторы и монады: do or do not, there is no try
Рассказали о функторах и монадах — мощных дополнениях для управления выполнением кода и обработки ошибок в фунциональном программировании.
619 открытий9К показов
Основными строительными блоками функционального программирования являются чистые функции. А побочные эффекты являются их злейшими врагами, потому что из-за них функции перестают быть чистыми.
К примеру, JSON.parse
практически обладает всеми признаками чистой функции: она всегда возвращает одинаковый результат при одинаковых входных параметрах и не зависит от глобального контекста. Но всё же в ней есть неприятный сюрприз в виде побочного эффекта.
Если передать невалидный JSON, то функция не просто не вернет результат, но поломает приложение.
Впрочем, такое поведение задумано по дизайну, и чтобы обойти эту проблему, нужно просто завернуть вызов функции в try/catch
:
При этом нам пришлось немного увеличить код и использовать мутабельную переменную. Использование let
не всегда желательно, так как потребуется более внимательный анализ кода, чтобы отследить все места, где переменная может быть изменена, особенно это усложняется при большем объеме кода.
Хотелось бы найти более элегантное решение для этой проблемы. Как вариант, можно попробовать добавить метод onError
и получить однострочное решение:
Выглядит красиво, но расширение объектов или функций через прототипы – это игра с огнем, к тому же это не универсальное решение. Каждый раз, когда нам нужно обрабатывать исключение, нам придется сначала изменить исходную функцию, а затем вызывать её. Звучит не очень.
Но что, если мы создадим функцию-обертку, которая временно расширит любую функцию с методом onError
, не изменяя саму функцию?
Другими словами, функция-обертка будет предоставлять свои методы взаймы, и по завершении выполнения функции, методы будут возвращены обратно владельцу.
А чтобы функция стала еще удобнее в использовании, можно добавить поддержку композиции функций, примерно так:
После чего, она стала сильно напоминать Promise
, не так ли? Правда, вместо метода onError
в Promise
используется метод catch
. В принципе, можно просто использовать Promise
без каких-либо дополнительных функций оберток:
Такая последовательность функций с использованием then
и catch
позволяет наглядно управлять потоком и обработкой ошибок. Однако есть небольшая проблема: теперь функция parseName
превратилась в асинхронную, значения к переменной будут присвоены не сразу после выполнения функции, а на следующем тике цикла событий (event loop). Это не совсем то, что хотелось бы.
К счастью, мы можем реализовать функцию, которая будет использовать те же интерфейсы, что и Promise
, но будет работать синхронно.
Ниже приведен пример наглядного использования. Здесь следует отметить, что для прерывания цепочки и вызова исполнения функции необходимо использовать метод finally
:
Интересный факт если вызвать функцию flow
с await
то она может выполнится без вызова finally
, хотя функция синхронная.
Наша функция выглядит и ведет себя как асинхронная функция, но на практике она не поддерживает асинхронный код. Это может ввести в заблуждение. Чтобы избежать путаницы, предлагаю добавить поддержку асинхронности следующим образом:
И теперь мы можем комбинировать асинхронные функции в нашем конвейере (pipeline) и не беспокоится где начинается синхронный код, а где асинхронный.
Функторы и монады
Незаметно для себя, от примера к примеру мы стали применять функторы. Если вы удивлены этим фактом, то спешу сообщить, что функции future, flow, fail
являются теми самыми функторами.
Функторы представляют собой контейнер, который хранит значение. Через функцию, переданную в методы then/catch
, мы сможем иметь доступ к значению и изменить его при желании.
Функтор flow
рекурсивно вызывает себя, передавая новое значение, именно поэтому мы можем бесконечно выстраивать цепочки из then/catch
, тем самым строить композиции из функций.
Более того, каждый из then/catch
обернуты в try/catch
, что позволяет безопасно вызывать функции и контролировать двумя ветками выполнения: успешной веткой (then) и веткой для обработки ошибок (catch). Как только нам понадобится итоговое значение, нужно вызывать функцию finally
. После вызова этой функции мы получим результат, и цепочка из then/catch
оборвется.
Примечательно, функция finally
может принять функцию, которую можно использовать для финального преобразования.
Вроде бы всё замечательно, но есть один момент. Если передать в наш поток другой поток, то на выходе мы получим не значение, а функтор.
Исправить это можно следующим образом:
Эти изменения делают flow
монадой, хотя, конечно, не идеальной, но уже есть основные признаки. Теперь мы сможем использовать вложенные потоки:
На практике вам вряд ли понадобится использовать монады в чистом виде. Скорее всего, вы будете использовать их внутри функций, которые будут возвращать значения из монады.
Однако есть случаи, когда использование функторов и монад в методах then/catch
может быть оправдано. С помощью функтора fail
мы можем прервать выполнение потока без генерации исключения.
Стоит отметить, что проброс исключения может быть более затратной операцией по сравнению с обычным вызовом функции.
Другой пример – это функтор halt
, который дает возможность прервать выполнение конвейера с определенным результатом.
Заключение
Является ли данное решение полной заменой для try/catch
? Это не замена, а скорее мощное дополнение для управления выполнением кода и обработки ошибок.
Этот подход удобен в случаях, когда функция может продолжить свое выполнение, несмотря на возникновение ошибки.
Кроме того, этот шаблон проектирования можно применять не только для обработки ошибок, но и для других целей. Вот некоторые возможные применения:
В этой статье я намеренно не следовал общепринятым конвенциям и законам монад.
Моя цель заключалась в том, чтобы познакомить читателей с альтернативным подходом к разработке и раскрыть красоту монады, которую обычно скрывают за академическими терминами и сложными конструкциями.
Конечно, одной статьей невозможно охватить все аспекты данного подхода, но я надеюсь, что полученная информация станет для вас отправной точкой в мир монад.
619 открытий9К показов