$30 off During Our Annual Pro Sale. View Details »

Going functional with algebraic effects

Going functional with algebraic effects

Introducing algebraic effects for Ruby with dry-effects

Nikita Shilnikov

September 28, 2019
Tweet

More Decks by Nikita Shilnikov

Other Decks in Technology

Transcript

  1. None
  2. Going Functional with Algebraic Effects

  3. ! 3/118

  4. Me Nikita Shilnikov — I write code — I write

    code in Ruby 4/118
  5. Algebraic effects? 5/118

  6. #hype 6/118

  7. Algebraic effects are a new burrito 7/118

  8. 8/118

  9. Already in production 9/118

  10. React is wired with algebraic effects 10/118

  11. Functional programming? 11/118

  12. f = !" x { x + 5 } 12/118

  13. Integer !" Integer f = !# x { x +

    5 } 13/118
  14. f = !" x { puts(x); x + 5 }

    14/118
  15. Launching rockets problem 15/118

  16. doSomethingNice !" Integer !# IO () 16/118

  17. Integer !" Integer, ? f = !# x { puts(x);

    x + 5 } 17/118
  18. Integer !" Puts[Integer] : Integer f = !# x {

    puts(x); x + 5 } 18/118
  19. Integer !" Puts[Integer] : Integer ^^^^^^^^^^^^^ f = !# x

    { puts(x); x + 5 } 19/118
  20. Puts[Integer] is an effect 20/118

  21. g = !" x { x + get(:y) } 21/118

  22. Integer !" Get[Symbol !" Integer] : Integer g = !#

    x { x + get(:y) } 22/118
  23. Integer !" Get[Symbol !" Integer] : Integer ^^^^^^^^^^^^^^^^^^^^^^ g =

    !# x { x + get(:y) } 23/118
  24. Effects in type signatures reveal code's intentions 24/118

  25. Effects are not side effects 25/118

  26. f = !" x { puts(x); x + 5 }

    Side effects: Integer !" Integer Effects: Integer !" Puts[Integer] : Integer 26/118
  27. To run effectful code you'll need a handler 27/118

  28. f = !" x { puts(x); x + 5 }

    with_puts { f.(10) } 28/118
  29. f = !" x { puts(x); x + 5 }

    with_puts { f.(10) } ^^^^^^^^^ 29/118
  30. Every effect must have a handler 30/118

  31. f = !" x { puts(x); x + 5 }

    f.(10) # !# Error! 31/118
  32. The World 32/118

  33. def greet print "Hello" end def main handle_print { greet

    } end 33/118
  34. main handle_print greet print 34/118

  35. World main # | handle_print # | greet # |

    print # ↓ 35/118
  36. World main # | handle_print # | * greet #

    | ↑ print # ↓ | 36/118
  37. World main # | handle_print # | * | greet

    # | ↑ | print # ↓ | ↓ 37/118
  38. World main # | ↑ | # | | |

    greet # | | | print # ↓ | ↓ 38/118
  39. World is the Handler 39/118

  40. It's a side effect when it's handled by the World

    40/118
  41. Examples 41/118

  42. Passing pseudo-global values around 42/118

  43. class SetLocaleMiddleware def call(env) locale = detect_locale(env) with_locale(locale) { @app.(env)

    } end end 43/118
  44. Testing features 44/118

  45. Testing features class RenderView def call(values) if feature? render_with_feature(values) else

    render_without_feature(values) end end end 45/118
  46. Testing features def call(env) feature_response, no_feature_response = with_feature do @app.(env)

    end if feature_response !" no_feature_response # !!# end end 46/118
  47. Testing features def call(env) feature_response, no_feature_response = with_feature do @app.(env)

    end if feature_response !" no_feature_response # !!# end end 47/118
  48. dry-effects 48/118

  49. Controlling time 49/118

  50. Accessing current time class CreatePost include Dry!"Effects.CurrentTime def call(values) publish_at

    = values[:publish_at] !# current_time # !!$ end end 50/118
  51. Providing current time class WithCurrentTime include Dry!"Effects!"Handler.CurrentTime def call(env) with_current_time

    { @app.(env) } end end 51/118
  52. Providing time class WithCurrentTime include Dry!"Effects!"Handler.CurrentTime def call(env) with_current_time {

    @app.(env) } end end 52/118
  53. Testing include Dry!"Effects!"Handler.CurrentTime example do with_current_time { !!# } end

    53/118
  54. Testing RSpec.configure do |c| c.include Dry!"Effects!"Handler.CurrentTime now = Time.now c.around

    do |ex| with_current_time(proc { now }, &ex) end end 54/118
  55. Testing example do next_day = Time.now + 86_400 with_current_time(proc {

    next_day }) { !!" } end 55/118
  56. Dependency injection 56/118

  57. Dependency injection class CreatePost include Dry!"Effects.Resolve(:post_repo) def call(values) if valid?(values)

    post_repo.create(values) else !!# end end end 57/118
  58. Dependency injection class CreatePost include Dry!"Effects.Resolve(:post_repo) def call(values) if valid?(values)

    post_repo.create(values) else !!# end end end 58/118
  59. Dependency injection class CreatePost include Dry!"Effects.Resolve(:post_repo) def call(values) if valid?(values)

    post_repo.create(values) else !!# end end end 59/118
  60. Making a container AppContainer = { post_repo: PostRepo.new, !!" }

    60/118
  61. Providing dependencies class ProvideApplication include Dry!"Effects!"Handler.Resolve(AppContainer) def call(env) provide {

    @app.(env) } end end 61/118
  62. Providing dependencies class ProvideApplication include Dry!"Effects!"Handler.Resolve(AppContainer) def call(env) provide {

    @app.(env) } end end 62/118
  63. Testing include Dry!"Effects!"Handler.Resolve example do post_repo = double(:post_repo) provide(post_repo: post_repo)

    do # !!# end end 63/118
  64. Tracing AppContainer = AppContainer.to_h do |key, value| [key, Wrapper.new(value)] end

    64/118
  65. Tracing AppContainer = AppContainer.to_h do |key, value| [key, Wrapper.new(value)] end

    65/118
  66. Batteries included Dry!"Effects.load_extensions(:system) class App < Dry!"Effects!"System!"Container Import = injector(!!#)

    end 66/118
  67. Frozen application Dry!"Effects.load_extensions(:system) class App < Dry!"Effects!"System!"Container Import = injector(!!#)

    end # boot.rb App.finalize! 67/118
  68. class CreateUser def initialize end def call end end 68/118

  69. State 69/118

  70. State class Add include Dry!"Effects.State(:result) def call(b) self.result += b

    nil end end 70/118
  71. State class Mult include Dry!"Effects.State(:result) def call(b) self.result *= b

    nil end end 71/118
  72. State class Calc include Dry!"Effects!"Handler.State(:result) def add Add.new end def

    mult Mult.new end end 72/118
  73. State def call(x, y, z) with_result(x) do add.(y) mult.(z) "

    ! " end end 73/118
  74. State calc = Calc.new calc.(5, 6, 7) 74/118

  75. State calc = Calc.new calc.(5, 6, 7) # !" (5

    + 6) * 7 !# 77 75/118
  76. State calc = Calc.new calc.(5, 6, 7) # !" [77,

    " ! "] 76/118
  77. All effects are composable* 77/118

  78. Composition class Program include Dry!"Effects.Cmp(:feature) include Dry!"Effects.State(:counter) def call if

    feature? self.counter += 2 "bye" else self.counter += 1 "hi!" end end end 78/118
  79. program = Program.new Dry!"Effects[:state, :counter].(10) do Dry!"Effects[:cmp, :feature].() do program.()

    end end # !# [13, ["hi!", "bye!"]] 79/118
  80. program = Program.new Dry!"Effects[:cmp, :feature].() do Dry!"Effects[:state, :counter].(10) do program.()

    end end # !# [[11, "hi!"], [12, "bye!"]] 80/118
  81. More examples 81/118

  82. Timeout class MakeRequest include Dry!"Monads[:try] include Dry!"Effects.Timeout(:http) def call(url) Try()

    { HTTParty.get(url, timeout: timeout) } end end 82/118
  83. Timeout class TimeoutMiddleware include Dry!"Effects!"Handler.Timeout(:http) def call(env) with_timeout(5.0) { @app.(env)

    } end end 83/118
  84. Parallel class PullData include Dry!"Effects.Parallel def call(urls) join(urls.map { |url|

    par { make_request.(url) } }) end end 84/118
  85. Parallel class PullData include Dry!"Effects.Parallel def call(urls) join(urls.map { |url|

    par { make_request.(url) } }) ^^^^ ^^^ end 85/118
  86. Parallel class ParallelMiddleware include Dry!"Effects!"Handler.Parallel def call(env) with_parallel.() { @app.(env)

    } end end 86/118
  87. dry-effects is a practical- oriented implementation 87/118

  88. v0.1 Cache, Cmp, CurrentTime, Defer, Env, Implicit, Interrupt, Lock, Parallel,

    Random, Reader, Resolve, Retry, State, Timeout, Timestamp 88/118
  89. Effects almost don't affect existing code 89/118

  90. Just like monads, effects are language agnostic 90/118

  91. 91/118

  92. Many shades of algebraic effects 92/118

  93. Level 0 93/118

  94. React 94/118

  95. React.useState Dry!"Effects.CurrentTime 95/118

  96. Level 1 96/118

  97. dry-effects 97/118

  98. Dry!"Effects.Retry Dry!"Effects.Parallel 98/118

  99. Level 2 99/118

  100. Fibers 100/118

  101. Async/await 101/118

  102. Async/await server.rb # scheduler !!" # ↑ user_repo.rb # find_user

    102/118
  103. Async/await server.rb # scheduler !!" # ↑ ↓ user_repo.rb #

    find_user 103/118
  104. Level 3 104/118

  105. Multi-shot continuations server.rb # scheduler !!" # ↑ ↓ ↓

    ↓ user_repo.rb # find_user 105/118
  106. Multi-shot continuations allow backtracking, parsers, etc. 106/118

  107. callcc / Fiber#dup 107/118

  108. Level 4 108/118

  109. Typed effects 109/118

  110. Algebraic effects are coming 110/118

  111. It works Trust me! 111/118

  112. Pros — New abilities — Easy to use — Already

    works (React!) — Easy to test — Traceable effects 112/118
  113. Cons — Unfamiliar — Can be overused — Can be

    abused — Require glue code with threading 113/118
  114. Next steps for dry-effects — Add async/await — Polishing APIs

    — More integrations with existing gems — More docs and examples — Multi-shot continuations? 114/118
  115. It's not all 115/118

  116. Learn more — github.com/topics/algebraic-effects — github.com/yallop/effects-bibliography 116/118

  117. Thank you — twitter.com/NikitaShilnikov — dry-rb.org/gems/dry-effects — dry-rb.org/gems/dry-system — github.com/dry-rb/dry-effects

    — t.me/flash_gordon 117/118
  118. Questions? 118/118