include Dry : : Monads[:result] def initialize(order) = @order = order def perform result = ApplicationRecord.transaction do deduct_from_user_account.bind { prepare_shipment.bind { update_order_status } }.tap { |result| raise ActiveRecord : : Rollback.new(result.failure) if result.failure? } end end … end начинаем транзакцию отменяем если что–то пошло не так попадем внутрь блока если прошлая операция успешна
@order.user.balance > @order.amount @order.user.deduct_amount(@order.amount) Right() else Left("cannot deduct # { @order.amount}, user has # { @order.user.balance}") end end def prepare_shipment @order.item_id = = 42 ? Success() : Failure("not enough items in warehouse") end def update_order_status @order.processed! Left() end end успех что–то пошло не так Реализация сервиса (а ля dry-monads)
dry-monads; • выполнение нескольких функций последовательно; • функция может вернуть только контейнер; • в зависимости от контейнера дальнейшие шаги могут не вызываться. 7
Either attr_reader :error def initialize(error) = @error = error def deconstruct = [@error] end class Right < Either attr_reader :value def initialize(value) = @value = value def deconstruct = [@value] end end • Right — «всё хорошо», в контейнере значение; • Left — «что–то пошло не так», в контейнере ошибка; • похоже на Maybe, но с объяснением, что случилось.
if user_id = = 42 Either : : Right.new("[email protected]") else Either : : Left.new("User # { user_id} not found") end end def format_email(either_email) case either_email in Either : : Right(email) then Either : : Right.new("Email: # { email}") in left then left end end format_email(fetch_email(42)) # = > #<Either : : Right:… @value="Email: [email protected]"> format_email(fetch_email(1)) # = > #<Either : : Left:… @error="User 1 not found"> ⚠ Такого кода будет много! Можно не перепаковывать?
fmap(&_fn) = raise NotImplementedError end class Either class Left # . . . class Right # . . . include Functor def fmap(&fn) case self in Either : : Right(value) then Either : : Right.new(fn.(value)) in left then left end end end • если значение Right — происходит распаковка, применение функции и запаковка; • Left остается без изменений.
аргументом; • curry: все функции могут принимать меньшее число аргументов и возвращать новые функции; • что будет, если передать функцию с двумя аргументами? 15
+ y Either : : Right.new(42).fmap(&method(:sum)) # = > #<Either : : Right: . . . @value=#<Proc: . . . (lambda) > > Either : : Right.new(42).fmap(&method(:sum)).value.(1) # = > 43 ^ a - > b - > c m b - > c m a m
к значению в контейнере; • pure заворачивает значение в минимально простой контейнер; • ^ достает функцию из контейнера слева и применяет к значению в контейнере справа, затем кладет всё в исходный контейнер (если сможет!). 17 module Applicative include Functor def self.included(klass) klass.extend(Module.new do def pure(_value) = raise NotImplementedError end) end def pure(value) = self.class.pure(value) def ^(_other) = raise NotImplementedError end
# . . . include Applicative def self.pure(value) = Right.new(value) def ^(other) case self in Right(fn) then other.fmap(&fn) in left then left end end end • pure — кладет значение в Right; • ^ вернет Left если Left будет слева; • иначе — обычный fmap.
заворачивает значение в монаду (обычно так же как pure); • >>= (bind) берет монаду и функцию, преобразующую текущее значение в другое, применяет функцию и заворачивает результат в монаду; • отличие от Applicative — цепочка bind позволяет видеть все предыдущие результаты. a b a - >
Result и подобные; • в функциональных языках контейнеров гораздо больше; • аппликативных функторов больше, чем монад; • если реализация (аппликативного) функтора следует законам — можно делать интересные надстройки. 27
include Traversable def traverse(applicative_class, &fn) return applicative_class : : pure([]) if empty? x, * xs = self applicative_class : : pure(lambda { |ta, rest| [ta] + rest }) ^ fn.(x) ^ ApplicativeArray.new(xs).traverse(applicative_class, &fn) end end 🌎 https: / / cutt.ly/zCtfXFq Вытаскивает первый Left наружу либо всё значения Right обернутые в Right
: Monads[:result] def initialize(order) = @order = order def perform ApplicationRecord.transaction do deduct_from_user_account.bind { prepare_shipment.bind { update_order_status } }.tap { |result| raise ActiveRecord : : Rollback.new(result.failure) if result.failure? } end end private def deduct_from_user_account; … end def prepare_shipment; … end def update_order_status; … end end
они могут гораздо больше; • монады могут быть реализованы на базе аппликативных функторов; • реализации аппликативных функторов могут давать другое интересное поведение. 38
сегодня видели и который не успели посмотреть (https:/ /github.com/DmitryTsepelev/applicative-rb); • моя статья про функторы в Haskell (https:/ /dmitrytsepelev.dev/haskell- adventures-functors); • не моя научпоп–статья про функторы, аппликативные функторы и монады в картинках (https:/ /adit.io/posts/2013-04-17- functors,_applicatives,_and_monads_in_pictures.html); • не моя хардкорная статья про аппликативные функторы (https:/ / www.sta ff .city.ac.uk/~ross/papers/Applicative.pdf). 39