Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Аппликативное программирование в Ruby: секретны...

Аппликативное программирование в Ruby: секретные архивы тайного общества адептов raleway–программирования

Avatar for Dmitry Tsepelev

Dmitry Tsepelev

November 16, 2022
Tweet

More Decks by Dmitry Tsepelev

Other Decks in Programming

Transcript

  1. DmitryTsepelev RUBYRUSSIA’22 Что хотим сделать 4 • списать деньги со

    счета; • проверить наличие товара; • обновить статус заказа; • если что–то не получается — все отменить.
  2. DmitryTsepelev RUBYRUSSIA’22 Реализация сервиса (а ля dry-monads) 5 class ProcessOrder

    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 начинаем транзакцию отменяем если что–то пошло не так попадем внутрь блока если прошлая операция успешна
  3. DmitryTsepelev RUBYRUSSIA’22 6 class ProcessOrder … private def deduct_from_user_account if

    @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)
  4. DmitryTsepelev RUBYRUSSIA’22 Программирование в стиле Railway • пока забудем про

    dry-monads; • выполнение нескольких функций последовательно; • функция может вернуть только контейнер; • в зависимости от контейнера дальнейшие шаги могут не вызываться. 7
  5. DmitryTsepelev RUBYRUSSIA’22 Те самые rails (но не те которые вы

    подумали 🙂) 8 class ProcessOrder 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 end Success Failure
  6. DmitryTsepelev RUBYRUSSIA’22 Контейнер Either 9 class Either class Left <

    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, но с объяснением, что случилось.
  7. DmitryTsepelev RUBYRUSSIA’22 Работа со значением в контейнере 10 def fetch_email(user_id)

    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"> ⚠ Такого кода будет много! Можно не перепаковывать?
  8. DmitryTsepelev RUBYRUSSIA’22 Интерфейс Functor 12 • преобразовывает значение в коробке

    с учетом типа контейнера; • обязательная функция — fmap. fmap a m b m
  9. DmitryTsepelev RUBYRUSSIA’22 Реализация Functor для Either 13 module Functor def

    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 остается без изменений.
  10. DmitryTsepelev RUBYRUSSIA’22 Реализация Functor для Either 14 def fetch_email(user_id) 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) = either_email.fmap { |email| "Email: # { email}" }
  11. DmitryTsepelev RUBYRUSSIA’22 Аппликативные функторы • fmap принимает функцию с одним

    аргументом; • curry: все функции могут принимать меньшее число аргументов и возвращать новые функции; • что будет, если передать функцию с двумя аргументами? 15
  12. DmitryTsepelev RUBYRUSSIA’22 Аппликативные функторы 16 def sum(x, y) = x

    + 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
  13. DmitryTsepelev RUBYRUSSIA’22 Applicative • позволяет удобно применять функцию в контейнере

    к значению в контейнере; • 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
  14. DmitryTsepelev RUBYRUSSIA’22 Реализация Applicative Functor для Either 18 class Either

    # . . . 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.
  15. DmitryTsepelev RUBYRUSSIA’22 Реализация Applicative Functor для Either 19 def format_email(either_email)

    add_label = lambda { |label, email| " # { label} : # { email}" } Either.pure(add_label) ^ Either : : Right.new("Email") ^ either_email end 🌎 https: / / cutt.ly/FCtfSd7
  16. DmitryTsepelev RUBYRUSSIA’22 Чем круто? • безопасное каррирование; • можно создавать

    функции, работающие только с «хорошим сценарием»; • плохой сценарий будет обработан реализацией аппликативного функтора для используемого типа. 20
  17. DmitryTsepelev RUBYRUSSIA’22 Monad 22 bind m b m • return

    заворачивает значение в монаду (обычно так же как pure); • >>= (bind) берет монаду и функцию, преобразующую текущее значение в другое, применяет функцию и заворачивает результат в монаду; • отличие от Applicative — цепочка bind позволяет видеть все предыдущие результаты. a b a - >
  18. DmitryTsepelev RUBYRUSSIA’22 Модуль Monad 23 module Monad include Applicative def

    self.included(klass) klass.extend(Module.new do def returnM(value) = pure(value) end) end def bind(&fn) = raise NotImplementedError end
  19. DmitryTsepelev RUBYRUSSIA’22 Реализация Monad для Either 24 class Either include

    Monad def bind(&fn) case self in Right(value) then fn.(value) in left then left end end end
  20. DmitryTsepelev RUBYRUSSIA’22 Monad 25 def fetch_email(user_id) case user_id when 42

    then Right("[email protected]") when 666 then Right("invalid") else Left("User # { user_id} not found") end end def validate(email) = email.include?(“@") ? Either : : returnM(email) : Left("invalid email") def format_email(email) = Right("Email: # { email}") def fetch_validate_and_format(user_id) fetch_email(user_id).bind { |email| validate(email).bind { |validated_email| format_email(validated_email) } } end fetch_validate_and_format(42) # = > #<Either : : Right:… @value="Email: [email protected]"> fetch_validate_and_format(666) # = > #<Either : : Left:… @error="invalid email"> fetch_validate_and_format(1) # = > #<Either : : Left:… @error="User 1 not found">
  21. DmitryTsepelev RUBYRUSSIA’22 Еще есть поведение! • самые популярные контейнеры —

    Result и подобные; • в функциональных языках контейнеров гораздо больше; • аппликативных функторов больше, чем монад; • если реализация (аппликативного) функтора следует законам — можно делать интересные надстройки. 27
  22. DmitryTsepelev RUBYRUSSIA’22 Реализация Functor/Applicative для списка 28 class ApplicativeArray <

    Array include Functor def fmap(&fn) = map(&fn.curry) include Applicative class < < self def pure(x) = ApplicativeArray.new([x]) end def ^(other) ApplicativeArray.new(flat_map { |fn| other.fmap(&fn) }) end end 🌎 https: / / cutt.ly/ECtge4W
  23. DmitryTsepelev RUBYRUSSIA’22 ApplicativeArray 29 def plus(x, y) = x +

    y def mult(x, y) = x * y array_with_functions = ApplicativeArray.new([method(:plus), method(:mult)]) array_with_args = ApplicativeArray.new([2, 7]) array_with_args_2 = ApplicativeArray.new([3, 5]) array_with_functions ^ array_with_args ^ array_with_args_2 # = > [5, 7, 10, 12, 6, 10, 21, 35] 🌎 https: / / cutt.ly/ECtge4W
  24. DmitryTsepelev RUBYRUSSIA’22 А можно поэлементно? 30 plus = lambda {

    |x, y| x + y } mult = lambda { |x, y| x * y } functions = ZipList.new([plus, mult]) args = ZipList.new([2, 7]) args_2 = ZipList.new([3, 5]) (functions ^ args ^ args_2).list # = > [5, 35] # pure – бесконечный список functions = ZipList : : pure(plus) args = ZipList : : pure(2) args_2 = ZipList.new([4, 6, 8]) (functions ^ args ^ args_2).list.eager.to_a # = > [6, 8, 10] 🌎 https: / / cutt.ly/TCtf865
  25. DmitryTsepelev RUBYRUSSIA’22 Аппликативный парсер 31 parser = ( Parser.pure(lambda {

    |a, b, c| a + b + c }.curry) ^ (Parser.char('A') | Parser.char('B')) ^ Parser.char('C') ^ Parser.string("42") ).fmap(&:downcase) parser.parse("AC42D") # = > #<Either : : Right:… @value=#<Pair:… @fst="D", @snd="ac" > > parser.parse("BC42D") # = > #<Either : : Right:… @value=#<Pair:… @fst="D", @snd="bc" > > parser.parse("DCB") # = > #<Either : : Left:… @error="unexpected D"> 🌎 https: / / cutt.ly/iCtdAiU
  26. DmitryTsepelev RUBYRUSSIA’22 Traversable 32 module Traversable def traverse(traversable_class, &_fn) =

    raise NotImplementedError end t a b a - > f traverse b t f Traversable Applicative
  27. DmitryTsepelev RUBYRUSSIA’22 Traversable для списка 33 class ApplicativeArray < Array

    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
  28. DmitryTsepelev RUBYRUSSIA’22 Traversable для списка 34 increment = lambda {

    |maybe_value| maybe_value.fmap { |value| value + 1 } } rights = ApplicativeArray.new([Right(1), Right(3), Right(5)]) rights_and_lefts = ApplicativeArray.new([Right(1), Left("error")]) rights.traverse(Either, &increment) # = > #<Either : : Right: . . . @value=[2, 4, 6]> rights_and_lefts.traverse(Either, &increment) # = > #<Either : : Left: . . . @error=“error"> 🌎 https: / / cutt.ly/zCtfXFq Вытаскивает первый Left наружу либо всё значения Right обернутые в Right
  29. DmitryTsepelev RUBYRUSSIA’22 Как было 36 class ProcessOrder include Dry :

    : 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
  30. DmitryTsepelev RUBYRUSSIA’22 Как стало 37 class ProcessOrder < MultiStepService def

    initialize(order) = @order = order add_step :deduct_from_user_account add_step :prepare_shipment add_step :update_order_status def deduct_from_user_account; … end def prepare_shipment; … end def update_order_status; … end end 🌎 https: / / cutt.ly/CCtfMFn def identity(value) = value def Right(value = method(:identity)) = Either : : Right.new(value) def Left(error = method(:identity)) = Either : : Left.new(error) class MultiStepService class < < self def add_step(step) = steps < < step def steps = @steps | | = [] end def perform ApplicationRecord.transaction do self.class.steps.reduce(Right()) { |result, step| result ^ send(step) }.on_error { |error| raise ActiveRecord : : Rollback.new(error) } end end end
  31. DmitryTsepelev RUBYRUSSIA’22 Выводы • монады круто подходят для Railway–стиля, но

    они могут гораздо больше; • монады могут быть реализованы на базе аппликативных функторов; • реализации аппликативных функторов могут давать другое интересное поведение. 38
  32. DmitryTsepelev RUBYRUSSIA’22 Куда пойти дальше • весь код который мы

    сегодня видели и который не успели посмотреть (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