Skip to content

Instantly share code, notes, and snippets.

@flash-gordon
Last active September 29, 2019 10:14
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save flash-gordon/d427259dca1a9162fed18365df2a9ad3 to your computer and use it in GitHub Desktop.
Save flash-gordon/d427259dca1a9162fed18365df2a9ad3 to your computer and use it in GitHub Desktop.

slidenumbers: true slidecount: true code: auto(4), text-scale(1), Fira Code list: alignment(left), bullet-character(β€”)

[.slidenumbers: false]


[.slidenumbers: false]

Going Functional with Algebraic Effects


πŸ‘‹


[.list: alignment(center), bullet-character(β€”)]

Me

Nikita Shilnikov

  • I write code
  • I write code in Ruby

![left 7%](pictures/dry-rb-logo.png) ` `` `` ` ![right 22%](pictures/rom-logo.png)

Algebraic effects?


#hype


Algebraic effects are a new burrito


inline


Already in production


React is wired with algebraic effects

inline 30%


Functional programming?

inline 70%


[.code: text-scale(1.6)]

f = -> x { x + 5 }

[.code: text-scale(1.6)]

Integer => Integer

f = -> x { x + 5 }

[.code: text-scale(1.6)]

f = -> x { puts(x); x + 5 }

Launching rockets problem


[.code: text-scale(1.6)]

doSomethingNice :: Integer -> IO ()

[.code: text-scale(1.6)]

Integer => Integer, ?

f = -> x { puts(x); x + 5 }

[.code: text-scale(1.6)]

Integer => Puts[Integer] : Integer

f = -> x { puts(x); x + 5 }

[.code: text-scale(1.6)] [.code-highlight: 1-2]

Integer => Puts[Integer] : Integer
           ^^^^^^^^^^^^^
f = -> x { puts(x); x + 5 }

Puts[Integer] is an effect


[.code: text-scale(1.6)]

g = -> x { x + get(:y) }

[.code: text-scale(1.6)]

Integer => Get[Symbol => Integer] : Integer

g = -> x { x + get(:y) }

[.code: text-scale(1.6)] [.code-highlight: 1-2]

Integer => Get[Symbol => Integer] : Integer
           ^^^^^^^^^^^^^^^^^^^^^^
g = -> x { x + get(:y) }

Effects in type signatures
reveal code's intentions


Effects
are not
side effects


[.code: text-scale(1.3)] [.text: alignment(left)]

f = -> x { puts(x); x + 5 }

Side effects: Integer => Integer

Effects: Integer => Puts[Integer] : Integer


To run effectful code
you'll need a handler


[.code: text-scale(1.5)]

f = -> x { puts(x); x + 5 }

with_puts { f.(10) }

[.code: text-scale(1.5)] [.code-highlight: 2-3]

f = -> x { puts(x); x + 5 }

with_puts { f.(10) }
^^^^^^^^^

Every effect must have
a handler


[.code: text-scale(1.5)]

f = -> x { puts(x); x + 5 }

f.(10) # => Error!

The World


[.code: text-scale(1.6)]

def greet
  print "Hello"
end

def main
  handle_print { greet }
end

[.code: text-scale(1.8)]


main
handle_print
greet
print

[.code: text-scale(1.8)]

               World
main         # |
handle_print # |
greet        # |
print        # ↓

[.code: text-scale(1.8)]

               World
main         # |
handle_print # | *
greet        # | ↑
print        # ↓ |

[.code: text-scale(1.8)]

               World
main         # |
handle_print # | * |
greet        # | ↑ |
print        # ↓ | ↓

[.code: text-scale(1.8)]

               World
main         # | ↑ |
             # | | |
greet        # | | |
print        # ↓ | ↓

World is the Handler


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


Examples 🌈


Passing pseudo-global values around


[.code: text-scale(1.6)]

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

Testing features


[.code: text-scale(1.5)]

Testing features

class RenderView
  def call(values)
    if feature?
      render_with_feature(values)
    else
      render_without_feature(values)
    end
  end
end

[.code: text-scale(1.5)] [.code-highlight: 1-4,9]

Testing features

def call(env)
  feature_response, no_feature_response = with_feature do
    @app.(env)
  end

  if feature_response != no_feature_response
    # ...
  end
end

Testing features

def call(env)
  feature_response, no_feature_response = with_feature do
    @app.(env)
  end

  if feature_response != no_feature_response
    # ...
  end
end

dry-effects


Controlling time


Accessing current time

class CreatePost
  include Dry::Effects.CurrentTime

  def call(values)
    publish_at = values[:publish_at] || current_time
    # ...
  end
end

[.code-highlight: 1,4,6-7]

Providing current time

class WithCurrentTime
  include Dry::Effects::Handler.CurrentTime

  def call(env)
    with_current_time { @app.(env) }
  end
end

[.code-highlight: 2,5]

Providing time

class WithCurrentTime
  include Dry::Effects::Handler.CurrentTime

  def call(env)
    with_current_time { @app.(env) }
  end
end

Testing

include Dry::Effects::Handler.CurrentTime

example do
  with_current_time { ... }
end

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

Testing

example do
  next_day = Time.now + 86_400

  with_current_time(proc { next_day }) { ... }
end

Dependency injection


Dependency injection

class CreatePost
  include Dry::Effects.Resolve(:post_repo)

  def call(values)
    if valid?(values)
      post_repo.create(values)
    else
      ...
    end
  end
end

[.code-highlight: 2]

Dependency injection

class CreatePost
  include Dry::Effects.Resolve(:post_repo)

  def call(values)
    if valid?(values)
      post_repo.create(values)
    else
      ...
    end
  end
end

[.code-highlight: 2,6]

Dependency injection

class CreatePost
  include Dry::Effects.Resolve(:post_repo)

  def call(values)
    if valid?(values)
      post_repo.create(values)
    else
      ...
    end
  end
end

Making a container

AppContainer = {
  post_repo: PostRepo.new,
  ...
}

Providing dependencies

class ProvideApplication
  include Dry::Effects::Handler.Resolve(AppContainer)

  def call(env)
    provide { @app.(env) }
  end
end

[.code-highlight: 2,5]

Providing dependencies

class ProvideApplication
  include Dry::Effects::Handler.Resolve(AppContainer)

  def call(env)
    provide { @app.(env) }
  end
end

Testing

include Dry::Effects::Handler.Resolve

example do
  post_repo = double(:post_repo)
  provide(post_repo: post_repo) do
    # ...
  end
end

[.code: text-scale(1.5)]

Tracing

AppContainer = AppContainer.to_h do |key, value|
  [key, Wrapper.new(value)]
end

[.code: text-scale(1.5)]

Tracing

AppContainer = AppContainer.to_h do |key, value|
  [key, Wrapper.new(value)]
end

Batteries included

Dry::Effects.load_extensions(:system)

class App < Dry::Effects::System::Container
  Import = injector(...)
end

[.code-highlight: 7-9]

Frozen application

Dry::Effects.load_extensions(:system)

class App < Dry::Effects::System::Container
  Import = injector(...)
end

# boot.rb

App.finalize!

[.code: text-scale(1.5)]

class CreateUser
  def initialize
  end

  def call
  end
end

State


State

class Add
  include Dry::Effects.State(:result)

  def call(b)
    self.result += b
    nil
  end
end

State

class Mult
  include Dry::Effects.State(:result)

  def call(b)
    self.result *= b
    nil
  end
end

State

class Calc
  include Dry::Effects::Handler.State(:result)

  def add
    Add.new
  end

  def mult
    Mult.new
  end
end

State

def call(x, y, z)
  with_result(x) do
    add.(y)
    mult.(z)
    "🍏"
  end
end

State

calc = Calc.new
calc.(5, 6, 7)

State

calc = Calc.new
calc.(5, 6, 7) # => (5 + 6) * 7 == 77

State

calc = Calc.new
calc.(5, 6, 7)
# => [77, "🍏"]

All effects are composable*


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

program = Program.new

Dry::Effects[:state, :counter].(10) do
  Dry::Effects[:cmp, :feature].() do
    program.()
  end
end
# => [13, ["hi!", "bye!"]]

program = Program.new

Dry::Effects[:cmp, :feature].() do
  Dry::Effects[:state, :counter].(10) do
    program.()
  end
end
# => [[11, "hi!"], [12, "bye!"]]

More examples


Timeout

class MakeRequest
  include Dry::Monads[:try]
  include Dry::Effects.Timeout(:http)

  def call(url)
    Try() { HTTParty.get(url, timeout: timeout) }
  end
end

Timeout

class TimeoutMiddleware
  include Dry::Effects::Handler.Timeout(:http)

  def call(env)
    with_timeout(5.0) { @app.(env) }
  end
end

Parallel

class PullData
  include Dry::Effects.Parallel

  def call(urls)
    join(urls.map { |url| par { make_request.(url) } })
  end
end

[.code-highlight: 5-6]

Parallel

class PullData
  include Dry::Effects.Parallel

  def call(urls)
    join(urls.map { |url| par { make_request.(url) } })
    ^^^^                  ^^^
end

Parallel

class ParallelMiddleware
  include Dry::Effects::Handler.Parallel

  def call(env)
    with_parallel.() { @app.(env) }
  end
end

dry-effects is a practical-oriented implementation


v0.1

Cache, Cmp, CurrentTime, Defer, Env, Implicit, Interrupt, Lock, Parallel, Random, Reader, Resolve, Retry, State, Timeout, Timestamp


Effects almost don't affect existing code


Just like monads, effects are language agnostic


inline


Many shades of algebraic effects


Level 0


React


React.useState

Dry::Effects.CurrentTime


Level 1


dry-effects


Dry::Effects.Retry

Dry::Effects.Parallel


Level 2


Fibers


Async/await


Async/await

server.rb     # scheduler
...           #  ↑
user_repo.rb  # find_user

Async/await

server.rb     # scheduler
...           #  ↑  ↓
user_repo.rb  # find_user

Level 3


Multi-shot continuations

server.rb     # scheduler
...           #  ↑  ↓ ↓ ↓
user_repo.rb  # find_user

Multi-shot continuations allow
backtracking, parsers, etc.


callcc / Fiber#dup


Level 4


Typed effects


Algebraic effects are coming


It works

Trust me!


Pros

  • New abilities
  • Easy to use
  • Already works (React!)
  • Easy to test
  • Traceable effects

Cons

  • Unfamiliar
  • Can be overused
  • Can be abused
  • Require glue code with threading

Next steps for dry-effects

  • Add async/await
  • Polishing APIs
  • More integrations with existing gems
  • More docs and examples
  • Multi-shot continuations?

It's not all


Learn more


Thank you


Questions?

inline 90%

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment