В программировании много абстракций, которые кажутся сложными на первый взгляд, но после практики становятся понятнее. Например - монады.
Я видел разработчиков, которые бояться использовать монады, а код с использованием монад - сжигают на месте. Основная причина - убеждение, что для этого необходимо знать теорию категорий и хаскель, да и зачем лишние абстракции в проекте. Другие не понимают зачем эту абстракцию тащить в руби. При этом, люди используют монады каждый день и не подозревают этого (ссылки). Поэтому сегодня поговорим о практическом использовании монад в руби и зачем это нужно.
Не значит, что монады - серебряная пуля, которая решит любую проблему. Так же, это не значит, что использование монад - единственно верный способ написания кода.
Поэтому, цель текста - не объяснить что такое монада, а показать, как и где начать ее использовать . Не ждите функторов, аппликативных функторов, математических выкладок и подробного объяснения зачем каждая монада нужна. Только практика и императивное объяснение. Статьи для любопытных:
- Объяснение функторов и монад в картинках
- Объяснение в HaskellWiki
- A Fistful of Monads - Learn You a Haskell for Great Good!
Код
Встречаются места когда приходится работать с данными и состояниями, например валидны данные или нет, вернула бд данные и если да - какие это данные, etc. Например:
response = http.get(url, params)
if response[:status] == :success
user_repository.create(response[:body])
end
Пример не выглядит сложным, но с бизнес логикой - вложенность выходит из под контроля:
response = http.get(url, params)
if response[:status] == :success
validation_result = validator.call(response[:body])
if validation_result.valid?
if user = user_repository.create(response[:body])
NotificationWorker.perform_async(user.id)
end
else
validation_result.errors
end
else
response
end
Вариант с гардами мне показался сложнее для восприятия
Вспоминая railway programming, было бы здорово переписать наш пример с использованием последовательных шагов:
step :get_response # returns response or Failed result
step :validate # returns payload or error message
step :persist # returns user or nothing
step :notify # calls worker with user id
В случаях, когда данные из прошлого шага влияют на последовательность логики, приходят на помощь монады. Важно запомнить, что монада - объект с общим интерфейсом, в котором лежит значение. Ближайшая абстракция - коробка. В коробке лежит все, что поместиться. При этом, не открыв коробку - значение не получить. Коробки - разные, большие, маленькие, цветные, но каждая коробка открывается одинаково - просто подними крышку и посмотри что там.
Монада - инструмент, который позволяет создавать цепочки вызовов функций или методов без лишних проверок данных, которые возвращаются в предыдущем шаге. Монад много, но рассмотрим только популярные - Maybe
, Result
и Try
.
-
Maybe
- оборачивает значение вSome
или возвращаетNone
объект без значения. -
Result
- оборачивает значение вSuccess
илиFailure
. -
Try
- оборачивает вызов кода вResult
если не было эксепшенов и вError
, если код упал с ошибкой (которая ловится)
У каждой из монад есть 3 главных функции, fmap
, bind
и способ получить данные, которые содержит в себе монада.
fmap
- выполняет блок, если значение монады соответствуетSuccess
варианту, а результат выполнения блока оборачивает в ту же монаду, у которой он вызвался. Например:
Some(1).fmap(&:to_s) # => Some('1')
None().fmap(&:to_s) # => Nothing
bind
- аналогиченfmap
, только возвращается результат выполнения блока:
Some(1).bind(&:to_s) # => '1'
Some(1).bind { |value| Success(value) } # => Success('1')
None().bind(&:to_s) # => Nothing
В руби нет монад из коробки, но существуют гемы, которые реализуют монады:
Советую dry, как единственную поддерживаемую. К тому же, при использовании dry-validation можно легко конвертировать результат валидации в монаду, воспользовавшись экстеншеном:
Dry::Validation.load_extensions(:monads)
Это минимум, который нужен, чтобы начать использовать монады в руби приложении. Для закрепления - перепишем изначальный пример с использованием монад:
http.get(url, params) # теперь клиент возвращается Result Monad
# валидация возвращает Result, который используется для следующих вызовов
.bind { |body| validator.call(body).to_result }
# сохраняем в базу, если валидация вернула Success
.bind { |payload| Maybe(user_repository.create(payload)) }
# вызываем воркер, если сохранение вернет Some
.fmap { |user| NotificationWorker.perform_async(user.id) }
Кроме использования монад в бизнес логике, попробуйте эту абстракцию для обработки результата, который возвращается из бизнес логики. Как пример - вызов operation из экшена и последующая обработка результата в этом же экшене: cookie_box/show.rb
Что делать с результатом
При использовании dry-monads
можно:
- вызывать на прямую
success?
,failed?
илиvalue_or
; - использовать
dry-matcher
; - мой любимый вариант, использовать
case
;
Минусы
- В отличии от условий (
if
,unless
, etc) нельзя просто взять и использовать монаду. Если не знать в чем смысл абстракции и что значатbind
иfmap
- будет сложно понять код, который написан; - Использование монад может сильно усложнить код. Спасает опыт, а опыт получается в практике;
- Если хотите начать использовать монады в проекте, придется прорваться через ужас в глазах коллег (причина почему я написал этот текст);
Запомнить
- Монады - абстракция для чейна вызовов функций и следованию railway programming;
- Для использования монад не нужно математическое образование. Главное понять, что монада оборачивает данные в объекты с единым интерфейсом;
- Советую начать с -
Maybe
,Result
иTry
; fmap
иbind
- методы для чейна вызовов функций- Чрезмерное использование монад усложняет код, будьте осторожны и подходите к написанию кода с умом;
Полезное
Как итог, будут 2 варика кода, с манадами и там где надо лепить
rescue
руками, тк в данном случае все ошибки глотаютсяДаже указанном в примере с использованием манад в самом описании теряется семантика
От чего нельзя юзать более понятный и рубишный синтаксис, а не переть всякое из других языков?
Пример, как это все безобразие бы выглядело в моем понимании: