├── .gitignore ├── .travis.yml ├── README.md ├── config └── config.exs ├── lib ├── curry.ex ├── effect.ex └── queue.ex ├── mix.exs ├── mix.lock └── test └── spec ├── effect.spec.exs ├── queue.spec.exs └── spec_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | /doc 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | elixir: 3 | - 1.2.2 4 | otp_release: 5 | - 18.2.1 6 | env: 7 | - MIX_ENV=test 8 | script: mix coveralls.travis 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Extensible Effects 2 | 3 | Monadic, softly-typed, extensible effect handling in Elixir. 4 | 5 | ![build status](http://img.shields.io/travis/metalabdesign/effects/master.svg?style=flat) 6 | ![coverage](http://img.shields.io/coveralls/metalabdesign/effects/master.svg?style=flat) 7 | ![license](http://img.shields.io/hexpm/l/effects.svg?style=flat) 8 | ![version](http://img.shields.io/hexpm/v/effects.svg?style=flat) 9 | ![downloads](http://img.shields.io/hexpm/dt/effects.svg?style=flat) 10 | 11 | ## Overview 12 | 13 | Based on the incredible works of: 14 | * ["Freer Monads, More Extensible Effects"](http://okmij.org/ftp/Haskell/extensible/) - Oleg Kiselyov & Hiromi Ishii, 15 | * ["Applicative Effects in Free Monads"](http://elvishjerricco.github.io/2016/04/13/more-on-applicative-effects-in-free-monads.html) - Will Fancher. 16 | 17 | Effects: 18 | 19 | * Allow you to generate a DSL for you use-case: 20 | * Easy to read/understand 21 | * Easily extensible 22 | * Are modular: 23 | * Can combine interpreters, test separately, keep in _separate packages_ 24 | * Are held in a data structure. 25 | * Separate the semantics from execution: 26 | * Optimize before execution (eg. `addOne >>= subtractOne == id`) 27 | * Run different interpreters over the sequence: 28 | * Test interpreter 29 | * Effectful interpreter 30 | * Pure interpreter 31 | 32 | Including: 33 | 34 | * HTTP: [pipeline]. 35 | * GraphQL: [????]. 36 | 37 | ## Installation 38 | 39 | Add `effects` to your list of dependencies in `mix.exs`: 40 | 41 | ```elixir 42 | def deps do [ 43 | {:effects, "~> 0.1.0"}, 44 | ] end 45 | ``` 46 | 47 | and then run: 48 | 49 | ```sh 50 | mix deps.get 51 | ``` 52 | 53 | ## Usage 54 | 55 | Effects can be used to encapsulate and effectively reason about all of the operations or actions your application has. Although an effect is a broad term that can encompass any computation, they typically refer to interactions that happen outside of the scope of your application – IO, databases, API requests and so forth – since these operations fall outside of the actual business logic and are harder to test, reason about and chain together in pleasant ways. 56 | 57 | There are some similarities here between things like Java interfaces. 58 | 59 | ### Basics 60 | 61 | First you need to define an effect types for your application. Each effect you define represents a computation you could do; importantly they do NOT provide any means to actually do the computation. You can think of it almost as a function declaration with no body – only the arguments (enforcing the _what_, not the _how_); akin to an `interface` from Java. 62 | 63 | We will implement a simple `Say` effect which represents somehow sending a message to the user. 64 | 65 | ```elixir 66 | defmodule MessageEffect do 67 | @moduledoc """ 68 | A simple set of effect types. 69 | """ 70 | 71 | # A message effect can be a "Say". This is essentially a union type where 72 | # you would list all the effect types in the module. If there was a `Bark` 73 | # effect you would have `t :: Say.e | Bark.e`. 74 | @type t :: Say.e 75 | 76 | # Define the structure for your effect types. These will contain all the 77 | # information needed to execute your effect. 78 | defmodule Say do 79 | @moduledoc """ 80 | Effect for sending a message to the user. Includes the message to be 81 | delivered as a simple string. 82 | """ 83 | 84 | # One type is here for the structure type – what does it mean to be a 85 | # "Say" effect. The other type here is for the interpreter type – what 86 | # are the inputs and outputs when processing a "Say" effect. More will 87 | # be explained about this later. 88 | @type t :: %Say{message: string} 89 | @type e :: Effect.t(Say.t, string -> int) 90 | defstruct [:message] 91 | end 92 | 93 | # Define your effect constructors. These are the public methods people 94 | # using your effects will call. We use the `is_string` guard to provide 95 | # some type safety. `defeffect` adds some boilerplate code which we will 96 | # get into later. 97 | @spec say(string) :: MessageEffect.t 98 | defeffect say(message) when is_string(message) do 99 | %Say{message: message} 100 | end 101 | end 102 | ``` 103 | 104 | It's important to note that creating an effect is a structural thing – invoking `say("werp")` simply creates a structure representing the action (i.e. _roughly_ `%Say{message: "werp"}`). This is markedly different from something like a Java interface which does _NOT_ capture the actual action to be performed – it merely provides some type signature that the computation needs to match. 105 | 106 | Now that all our effect types are defined, we need someone who actually wants to use the `Say` effect we've provided. We'll provide a module that consumes our effect by greeting a user. 107 | 108 | ```elixir 109 | defmodule MessageApp do 110 | import MessageEffect 111 | 112 | @doc """ 113 | Say hello to a user. 114 | """ 115 | @spec say(string) :: MessageEffect.t 116 | def greet(name) do 117 | say "Hello " <> name 118 | end 119 | end 120 | ``` 121 | 122 | Again, anyone invoking `MessageApp.greet/1` will be able to _represent_ a `greet` computation, but not actually _perform_ it. Eventually, however, there needs to be a way to actually do something with the effect, and that's handled by an interpreter. 123 | 124 | We can define a simple such interpreter that outputs the results to the console. 125 | 126 | ```elixir 127 | defmodule MessageInterpreter.Console do 128 | @moduledoc """ 129 | Interpreter for MessageEffect. 130 | """ 131 | @spec handle(MessageEffect.Say.e) 132 | defeffect handle(%MessageEffect.Say{message: message}) do 133 | IO.puts(message) 134 | end 135 | end 136 | ``` 137 | 138 | So now we have a complete, but decomposed, application. Every action the application can perform is directly visible in its effect types, and the matter in which those actions are performed is directly visible in the interpreter. 139 | 140 | All that's left is to connect the dots: 141 | 142 | ```elixir 143 | # Create the computation. 144 | computation = MessageApp.greet "Fred" 145 | # Perform the computation. 146 | MessageInterpreter.Console.handle(computation) 147 | ``` 148 | 149 | ### Multiple Effects 150 | 151 | Naturally it would be helpful to be able to run more than a single effect. This can be achieved using the `then` operator `~>`: first do `effectA`, _then_ do `effectB`. 152 | 153 | ```elixir 154 | computation = MessageApp.greet "Fred" ~> MessageApp.greet "Carl" 155 | MessageInterpreter.Console.handle(computation) 156 | ``` 157 | 158 | This also means that your interpreter needs to be able to process multiple effects. 159 | 160 | ```elixir 161 | defmodule MessageInterpreter.Console do 162 | @moduledoc """ 163 | Interpreter for MessageEffect. 164 | """ 165 | @spec handle(MessageEffect.Say.t, MessageEffect.Say.i) :: 166 | defeffect handle(%MessageEffect.Say{message: message}) do 167 | IO.puts(message) 168 | # Run the next effect. 169 | next 170 | end 171 | end 172 | ``` 173 | 174 | An interesting property of this pattern is that it allows you to control the order of effect execution. If you wanted to print all the messages in reverse you could simply put `next` before your handler, for example: 175 | 176 | ```elixir 177 | defmodule MessageInterpreter.Console do 178 | @moduledoc """ 179 | Interpreter for MessageEffect. 180 | """ 181 | @spec handle(MessageEffect.Say.t, MessageEffect.Say.i) :: 182 | defeffect handle(%MessageEffect.Say{message: message}) do 183 | # Run the next effect. 184 | next 185 | # Output message to user. 186 | IO.puts(message) 187 | end 188 | end 189 | ``` 190 | 191 | ### Multiple Interpreters 192 | 193 | One of the benefits of using this kind of interpreter pattern is that we can build out different interpreters. Instead of sending messages to the console we can just as easily send them to a database or an HTTP endpoint. 194 | 195 | ```elixir 196 | defmodule MessageInterpreter.Tweet do 197 | @moduledoc """ 198 | 199 | """ 200 | defeffect handle(%Say{message: message}) do 201 | Twitter.post_tweet(message) 202 | next 203 | end 204 | end 205 | ``` 206 | 207 | As per the previous example, the logic in the application remains the same, only the interpreter is changed: 208 | 209 | ```elixir 210 | computation = MessageApp.greet "Fred" 211 | MessageInterpreter.Tweet.handle(computation) 212 | ``` 213 | 214 | ### Using Results from Effects 215 | 216 | Effects wouldn't be very useful if we couldn't do something with the result of performing one. Since effects themselves are not real computations, we need a way of saying "after you actually perform this effect, do something with the result we got back from the interpreter for this effect". 217 | 218 | In the case of our message app we will now provide a status response as the result of the `Say` effect – i.e. did we successfully deliver the message to the user or not. 219 | 220 | ```elixir 221 | defmodule MessageInterpreter.Console do 222 | @moduledoc """ 223 | Interpreter for MessageEffect. 224 | """ 225 | defeffect handle(%MessageEffect.Say{message: message}, next) do 226 | IO.puts(message) 227 | :ok # For console messages the result is always :ok 228 | end 229 | end 230 | ``` 231 | 232 | The simplest thing to do is just to use the result from the interpreter. We can check the result of our computation: 233 | 234 | ```elixir 235 | computation = MessageApp.greet "Fred" 236 | case MessageInterpreter.Console.handle(computation) do 237 | :ok -> IO.puts "Message sent!" 238 | _ -> IO.puts "Message not sent!" 239 | end 240 | ``` 241 | 242 | Many times, however, it is desirable to perform this kind of action as _part of_ the effect. We can incorporate this kind of cascading behavior using the monadic bind operator `~>>`: 243 | 244 | ```elixir 245 | defmodule MessageApp do 246 | def hello(name) do 247 | # Greet the user. Remember that `greeting` is NOT the result of executing 248 | # anything; it is _just_ the effect itself. 249 | greeting = MessageEffect.say "" <> name 250 | # So now we create a _new_ effect based on the result of `greeting` by 251 | # using `~>>`. Again, this results in a new effect and not an actual 252 | # computation, but this new effect encodes the old effect _AND_ some new 253 | # computation based upon the results of executing that old effect. 254 | greeting ~>> fn result -> 255 | case result do 256 | :ok -> MessageEffect.say "Message sent!" 257 | _ -> MessageEffect.say "Message not sent!" 258 | end 259 | end 260 | end 261 | end 262 | ``` 263 | 264 | While this example is itself a little contrived, such a pattern allows complex behavior and business logic to be built up (e.g. first do this, then, _based on the result_, do something else). 265 | 266 | **IMPORTANT**: The result you return from the function you give to `~>>` _MUST BE_ either: another effect (as seen above) _or_ a value wrapped in `Effect.pure`. 267 | 268 | ### Managing State 269 | 270 | One of the main reasons of using the effect pattern is to deal with the state that comes as the result of having effects. Previously the result of the effect was simply passed down the chain to the next effect – it turns out managing state is simply a generalization of this idea. 271 | 272 | Instead of having a single return value from an effect, a tuple can be passed – one value that is passed down to the next effect (akin to the previous return value) and another that is passed down to the next invocation of the interpreter (the state). 273 | 274 | We can use this, for example, to keep track of a quota for the number of messages we send out and stop sending them after a certain threshold has passed. 275 | 276 | ```elixir 277 | defmodule MessageInterpreter.Console do 278 | @moduledoc """ 279 | Interpreter for MessageEffect. 280 | """ 281 | @spec handle(MessageEffect.Say.t, MessageEffect.Say.i) :: 282 | defeffect handle(%MessageEffect.Say{message: message}) do 283 | if count < 3 do 284 | # Output message to user. 285 | IO.puts(message) 286 | next(count+1) 287 | else 288 | next(count) 289 | end 290 | end 291 | end 292 | ``` 293 | 294 | Now only the first few messages will be sent. 295 | 296 | ```elixir 297 | computation = Effect.pure(nil) 298 | ~> MessageApp.greet "1" 299 | ~> MessageApp.greet "2" 300 | ~> MessageApp.greet "3" 301 | ~> MessageApp.greet "4" 302 | 303 | case MessageInterpreter.Console.handle(computation) do 304 | {:ok, sent, total} -> IO.puts "All #{total} messages sent!" 305 | {_, sent, total} -> IO.puts "Only sent #{sent}/#{total} messages!" 306 | end 307 | ``` 308 | 309 | ### Effects and Testing 310 | 311 | Since effects can be used to separate the business logic from real world actions, testing can become much more straightforward – there is significantly diminished reliance on stubs, mocks and the like because you can simply write a test interpreter. 312 | 313 | ```elixir 314 | defmodule Test.MessageInterpreter do 315 | defeffect handle(msgs, %Say{message: "Hello fail" = msg}, next) do 316 | handle([msg|msgs], next.(:fail)) 317 | end 318 | defeffect handle(msgs, %Say{message: msg}, next) do 319 | handle([msg|msgs], next.(:ok)) 320 | end 321 | end 322 | ``` 323 | 324 | Then your tests just need to use that interpreter. 325 | 326 | ```elixir 327 | defmodule Test.MyApp do 328 | use ESpec 329 | it "should work for good messages" do 330 | expect TestMessageInterpreter.handle(MessageApp.hello("Bob")) 331 | |> to(contain "Success!") 332 | end 333 | it "should work for bad messages" do 334 | expect TestMessageInterpreter.handle(MessageApp.hello("James")) 335 | |> to(contain "Failure!") 336 | end 337 | end 338 | ``` 339 | 340 | ### Effect Parallelism 341 | 342 | Often the task of performing an effect can be a time-intensive one – accessing some file on disk, looking up a record in a database and so forth. Using effects offers you the ability to achieve maximal parallelism. 343 | 344 | ```elixir 345 | fn user, payments, tweets -> %{ 346 | name: user.name, 347 | balance: payments.balance, 348 | tweets: tweets, 349 | } end <<~ user(5) <<~ payments("foo") <<~ tweets("fred") 350 | ``` 351 | 352 | Because there is no direct dependency between `user`, `payments` and `tweets` all of them are free to be executed in parallel. 353 | 354 | 355 | ### Combining Effect Domains 356 | 357 | Sometimes you may wish to group together effects into some logical domain. For example you may have one group of effects responsible for payment handling and another group of effects responsible for sending notifications. This can be useful because _interpreters tend to be domain-specific_. You could have one payment interpreter for testing and one for real payments; you could have one notification interpreter that sends notifications to all kinds of services and another that just sends to email. By splitting the interpreters you can choose how certain groups of effects are handled. For local development you might want to have the test payments and only email notifications; for a staging server you might want the test payments and full notifications; for production you would want real payments and full notifications. As the number of effects in your application grows, splitting them into logical groups can make them both easier to deal with and prevent combinatorial explosion when you want to define new interpreters. 358 | 359 | The original Haskell implementation makes wonderful use of the open union type to ensure totality and extensibility when it comes to combining groups of effects. Elixir has no such luxury, but we can achieve something similar. 360 | 361 | Basically the combined interpreters' state is a tuple type, which each entry in the tuple corresponding to the state of the nth interpreter. When an effect is to be processed by the combined interpreter, the domain to which the effect belongs is checked and it is sent to the appropriate sub-interpreter responsible for said domain. The return values that produce new effects are passed across interpreter boundaries. 362 | 363 | ```elixir 364 | defmodule MultiInterpreter do 365 | # We handle effects of _either_ type A or B by combining two different 366 | # interpreters. 367 | @type t :: EffectA.t | EffectB.t 368 | 369 | Effect.Interpreter.combine( 370 | (if prod, then: InterpreterAReal, else: InterpreterATest), 371 | InterpreterB, 372 | ... 373 | ) 374 | end 375 | 376 | # Looks something like this internally 377 | def interp({state_a, state_b}, %Effect{domain: EffectsA} = eff) do 378 | {state_a |> interp_a(eff), state_b} 379 | end 380 | 381 | def interp(%Effect{domain: EffectsB} = eff) do 382 | {state_a, state_b |> interp_b(eff)} 383 | end 384 | ``` 385 | 386 | ### Configurable Interpreters 387 | 388 | Pass configuration options as part of state. 389 | 390 | ### Interpreter Chaining 391 | 392 | Pass the state from one interpreter to another. 393 | 394 | ```elixir 395 | initial_state 396 | |> Interpreter.handle(computationA) 397 | |> Interpreter.handle(computationB) 398 | ``` 399 | 400 | ## How it Works 401 | 402 | Explain `Effect.Pure`, `Effect.Effect`. 403 | Explain interpreter queue. 404 | 405 | ## Analogs and Other Design Patterns 406 | 407 | Although using the effect monad addresses a large class of problems it's worthing thinking about the question: Why would you (not) want to use effects instead of other design patterns? 408 | 409 | ### Versus the Actor Pattern 410 | 411 | Effect handling can be done, to some degree, using actors. The interpreters are actors and the messages they receive are the effects. 412 | 413 | ```elixir 414 | defmodule MyActorInterpreter do 415 | def loop(state) do 416 | msg = receive 417 | case msg do 418 | {:say, tag, message, from} -> 419 | IO.puts(message) 420 | send(tag, :ok) 421 | loop(new_state) 422 | end 423 | end 424 | end 425 | ``` 426 | 427 | While you get the great advantage of being able to easily swap out one actor for another (just like effect interpreters), composition is a bit unruly. There's also the overhead of sending messages. 428 | 429 | * Better for distributed systems 430 | * Higher latency 431 | * Sequencing is hard(er) 432 | 433 | ### Versus Protocols 434 | 435 | Protocols provide the same ability to swap between implementations. 436 | 437 | ```elixir 438 | defprotocol Sayer do 439 | def say(message) 440 | end 441 | 442 | defimpl MySayer, for: Sayer do 443 | def say(message) do 444 | IO.puts message 445 | :ok 446 | end 447 | end 448 | ``` 449 | 450 | More details here. 451 | 452 | * Lower latency 453 | * No state management 454 | 455 | ## Performance 456 | 457 | The little elephant in the room. The free(r) monad (on which this is based) is generally notorious for performing poorly due to the quadratic cost incurred from left monadic folds. Effects does not suffer from this due to its use of a fast queue with good left-fold characteristics. 458 | 459 | In the general case, however, interpreters are slower than running code directly; indeed every layer of abstraction typically comes with a cost like this. But the performance cost of your application will generally be dominated by your business logic and I/O operations – not a thin effects layer. 460 | 461 | [pipeline]: https://github.com/metalabdesign/pipeline 462 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | # This file is responsible for configuring your application 2 | # and its dependencies with the aid of the Mix.Config module. 3 | use Mix.Config 4 | 5 | # This configuration is loaded before any dependency and is restricted 6 | # to this project. If another project depends on this project, this 7 | # file won't be loaded nor affect the parent project. For this reason, 8 | # if you want to provide default values for your application for 9 | # 3rd-party users, it should be done in your "mix.exs" file. 10 | 11 | # You can configure for your application as: 12 | # 13 | # config :effects, key: :value 14 | # 15 | # And access this configuration in your application as: 16 | # 17 | # Application.get_env(:effects, :key) 18 | # 19 | # Or configure a 3rd-party app: 20 | # 21 | # config :logger, level: :info 22 | # 23 | 24 | # It is also possible to import configuration files, relative to this 25 | # directory. For example, you can emulate configuration per environment 26 | # by uncommenting the line below and defining dev.exs, test.exs and such. 27 | # Configuration from the imported file will override the ones defined 28 | # here (which is why it is important to import them last). 29 | # 30 | # import_config "#{Mix.env}.exs" 31 | -------------------------------------------------------------------------------- /lib/curry.ex: -------------------------------------------------------------------------------- 1 | defmodule Effects.Curry do 2 | @moduledoc """ 3 | Runtime function currying. Needed for making applicatives behave sensibly. It 4 | is not as fancy as algae's `defcurry` but it requires no changes to the 5 | calling code. 6 | 7 | From: http://blog.patrikstorm.com/function-currying-in-elixir 8 | """ 9 | 10 | @doc """ 11 | Curry the given function. 12 | """ 13 | def curry(fun) when is_function(fun, 1) do 14 | fun 15 | end 16 | 17 | def curry(fun) do 18 | {_, arity} = :erlang.fun_info(fun, :arity) 19 | curry(fun, arity, []) 20 | end 21 | 22 | def curry(fun, 0, arguments) do 23 | apply(fun, Enum.reverse arguments) 24 | end 25 | 26 | def curry(fun, arity, arguments) do 27 | fn arg -> curry(fun, arity - 1, [arg | arguments]) end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /lib/effect.ex: -------------------------------------------------------------------------------- 1 | defmodule Effects do 2 | @moduledoc """ 3 | Implementation of the extensible effects monad in Elixir. 4 | See: http://okmij.org/ftp/Haskell/extensible/ 5 | """ 6 | @type t(effects) :: Pure.t(any) | Effect.t(effects) 7 | 8 | import Effects.Curry 9 | alias Effects.Queue, as: Q 10 | 11 | defmodule Pure do 12 | @moduledoc """ 13 | Explain pure. 14 | """ 15 | @type t(type) :: %Pure{value: type} 16 | defstruct [:value] 17 | end 18 | 19 | defmodule Effect do 20 | @moduledoc """ 21 | Explain ze effect. 22 | """ 23 | @type t(x, i, o) :: %Effect{effect: x, next: Q.t(i, o)} 24 | defstruct [:domain, :effect, :next] 25 | end 26 | 27 | # ---------------------------------------------------------- 28 | # Constructors 29 | # Create new instances of "Effect". 30 | # ---------------------------------------------------------- 31 | @doc """ 32 | Create a new pure value. 33 | """ 34 | @spec pure(type) :: Effect.t(any) when type: any 35 | def pure(value) do 36 | %Pure{value: value} 37 | end 38 | 39 | @doc """ 40 | Create a new effect value. 41 | """ 42 | @spec effect(atom, any, any) :: Effect.t(any) 43 | def effect(domain, effect, next) do 44 | %Effect{domain: domain, effect: effect, next: next} 45 | end 46 | 47 | 48 | # ---------------------------------------------------------- 49 | # Functor 50 | # ---------------------------------------------------------- 51 | @spec fmap(Effect.t(any), (... -> any)) :: Effect.t(any) 52 | @doc """ 53 | 54 | """ 55 | def fmap(%Pure{value: value}, f) when is_function(f) do 56 | pure(f.(value)) 57 | end 58 | def fmap(%Effect{next: next} = effect, f) when is_function(f) do 59 | %Effect{effect | next: next |> Q.append(&pure(f.(&1)))} 60 | end 61 | 62 | 63 | # ---------------------------------------------------------- 64 | # Applicative 65 | # ---------------------------------------------------------- 66 | @spec fmap(Effect.t(any), Effect.t((... -> any))) :: Effect.t(any) 67 | @doc """ 68 | 69 | """ 70 | def ap(%Pure{value: f}, %Pure{value: x}) do 71 | pure(curry(f).(x)) 72 | end 73 | def ap(%Pure{value: f}, %Effect{next: next} = effect) do 74 | %Effect{effect | next: next |> Q.append(&pure(curry(f).(&1)))} 75 | end 76 | def ap(%Effect{next: next} = effect, %Pure{value: x}) do 77 | %Effect{effect | next: next |> Q.append(&pure(curry(&1).(x)))} 78 | end 79 | def ap(%Effect{next: next} = effect, target) do 80 | %Effect{effect | next: next |> Q.append(&fmap(target, curry(&1)))} 81 | end 82 | def ap(f, free) when is_function(f) do 83 | ap(pure(f), free) 84 | end 85 | 86 | # ---------------------------------------------------------- 87 | # Monad 88 | # ---------------------------------------------------------- 89 | @spec fmap(Effect.t(any), (... -> Effect.t(any))) :: Effect.t(any) 90 | @doc """ 91 | 92 | """ 93 | def bind(%Pure{value: value}, f) when is_function(f) do 94 | f.(value) 95 | end 96 | def bind(%Effect{next: next} = effect, f) when is_function(f) do 97 | %Effect{effect | next: next |> Q.append(f)} 98 | end 99 | 100 | 101 | # ---------------------------------------------------------- 102 | # Interpreter 103 | # ---------------------------------------------------------- 104 | def queue_apply(list, x) do 105 | case Q.pop(list) do 106 | {k} -> k.(x) 107 | {k, t} -> herp(k.(x), t) 108 | end 109 | end 110 | 111 | 112 | # use `task = Task.async(handler)` and Task.await(task) to deal with the 113 | # applicative effects. 114 | 115 | defp herp(%Pure{value: value}, k) do 116 | queue_apply(k, value) 117 | end 118 | defp herp(%Effect{next: next} = effect, k) do 119 | %Effect{effect | next: Q.concat(next, k)} 120 | end 121 | 122 | # ---------------------------------------------------------- 123 | # Shorthand operators 124 | # ---------------------------------------------------------- 125 | 126 | @doc """ 127 | The `then` operator. Equivalent to `>>` in Haskell. 128 | """ 129 | def a ~> b do 130 | bind(a, fn _ -> b end) 131 | end 132 | 133 | @doc """ 134 | The `bind` operator. Equivalent to `>>=` in Haskell. 135 | """ 136 | def a ~>> b do 137 | bind(a, b) 138 | end 139 | 140 | @doc """ 141 | The `apply` operator. Equivalent to `<*>` in Haskell. 142 | """ 143 | def a <<~ b do 144 | ap(a, b) 145 | end 146 | 147 | @doc """ 148 | Allow your module to use the `defeffect` macro. 149 | """ 150 | defmacro __using__(_) do 151 | quote do 152 | import Effects 153 | alias Effects.Pure 154 | alias Effects.Effect 155 | end 156 | end 157 | 158 | @doc """ 159 | This is essentially `liftF` except written as a macro. It allows for creating 160 | lifted functions in a trivial manner. 161 | 162 | defeffect my_effect(param1, param2), do: %MyEffect{...} 163 | 164 | Theoretically we could also add `defeffectp` but private effects would kind 165 | of defeat the purpose I think... 166 | """ 167 | defmacro defeffect(head, do: body) do 168 | quote do 169 | def unquote(head) do 170 | %Effect{ 171 | domain: __MODULE__, 172 | effect: unquote(body), 173 | next: Q.value(&pure/1) 174 | } 175 | end 176 | end 177 | end 178 | end 179 | -------------------------------------------------------------------------------- /lib/queue.ex: -------------------------------------------------------------------------------- 1 | defmodule Effects.Queue do 2 | @moduledoc """ 3 | Queue used internally by Effects for collecting a sequence of binds. 4 | 5 | A queue with the following characteristics: 6 | 7 | * non-empty by construction 8 | * concatenation of two queues: O(1) 9 | * enqueuing an item to the queue: O(1) 10 | * dequeuing an item from the queue: ~O(1) 11 | * is type-aligned 12 | * only contains functions 13 | 14 | As usual, some Haskell cleverness that has gone underappreciated for some time is now available in Elixir. Everything here is fairly trivial with the sole 15 | exception of how `dequeue` manages to achieve `O(1)` _generally_. 16 | 17 | As the queue is built up, it forms a left-leaning tree. This is because all 18 | appends put their leaves on the right child of a node. It makes it quick and 19 | easy to add new nodes. The problem occurs when you want to start popping items 20 | out of the queue – your first entry is now at the very bottom of the tree. So 21 | to get around having to traverse the whole queue every time the dequeue code 22 | structurally reverses the queue – a new queue is built up that moves the 23 | leaves soonest to be popped to the top. This new queue is then repeatedly 24 | used when dequeuing more values. The initial cost for this reversal is O(n) 25 | but all subsequent calls are O(1). 26 | 27 | See: http://okmij.org/ftp/Haskell/Reflection.html 28 | """ 29 | 30 | alias Effects.Queue, as: Q 31 | 32 | @type t(i, o) :: Q.Leaf.t(i, o) | Q.Node.t(i, o) 33 | 34 | defmodule Node do 35 | @moduledoc """ 36 | 37 | """ 38 | @type t(i, o) :: %Node{ 39 | left: Q.t(i, any), 40 | right: Q.t(any, o), 41 | } 42 | defstruct [:left, :right] 43 | end 44 | 45 | defmodule Leaf do 46 | @moduledoc """ 47 | 48 | """ 49 | @type t(i, o) :: %Leaf{ 50 | value: (i -> o) 51 | } 52 | defstruct [:value] 53 | end 54 | 55 | @doc """ 56 | Constructor for new leaves. 57 | """ 58 | @spec value((i -> o)) :: Q.t(i, o) when i: any, o: any 59 | def value(value) do 60 | %Leaf{value: value} 61 | end 62 | 63 | @doc """ 64 | Concatenate two queues together. 65 | """ 66 | @spec concat(Q.t(i, x), Q.t(x, o)) :: Q.t(i, o) when i: any, o: any, x: any 67 | def concat(a, b) do 68 | %Node{left: a, right: b} 69 | end 70 | 71 | @doc """ 72 | Append a value to the queue. 73 | """ 74 | @spec append(Q.t(i, x), (x -> o)) :: Q.t(i, o) when i: any, o: any, x: any 75 | def append(t, v) do 76 | concat(t, value(v)) 77 | end 78 | 79 | @doc """ 80 | Convert queue to a list. Loses type-alignment. 81 | """ 82 | # @spec to_list(Q.t(i, o)) :: [(... -> any)] when i: any, o: any 83 | def to_list(queue) do 84 | case pop(queue) do 85 | {value} -> [value] 86 | {value, rest} -> [value|to_list(rest)] 87 | end 88 | end 89 | 90 | @doc """ 91 | Remove an element from the queue, returning a tuple with a single element if 92 | the queue is now empty, or a tuple with two elements if the queue is not. The 93 | first element in the tuple is the popped value from the queue, the second is 94 | the remainder of the queue. 95 | """ 96 | @spec pop(Q.t(i, o)) :: {(i -> o)} | {(i -> x), Q.t(x, o)} when i: any, o: any, x: any 97 | def pop(%Leaf{value: value}) do 98 | {value} 99 | end 100 | def pop(%Node{left: left, right: right}) do 101 | pop(left, right) 102 | end 103 | 104 | @spec pop(Q.t(i, x), Q.t(x, o)) :: {(i -> x), Q.t(x, o)} when i: any, o: any, x: any 105 | defp pop(%Leaf{value: value}, rest) do 106 | {value, rest} 107 | end 108 | defp pop(%Node{left: left, right: right}, rest) do 109 | pop(left, concat(right, rest)) 110 | end 111 | 112 | end 113 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Effects.Mixfile do 2 | use Mix.Project 3 | 4 | def project do [ 5 | app: :effects, 6 | version: "0.1.1", 7 | description: description, 8 | package: package, 9 | elixir: "~> 1.2", 10 | build_embedded: Mix.env == :prod, 11 | start_permanent: Mix.env == :prod, 12 | deps: deps, 13 | spec_pattern: "*.spec.exs", 14 | aliases: aliases, 15 | spec_paths: [ 16 | "test/spec", 17 | ], 18 | test_coverage: [ 19 | tool: ExCoveralls, 20 | test_task: "espec", 21 | ], 22 | preferred_cli_env: [ 23 | "espec": :test, 24 | "coveralls": :test, 25 | "coveralls.detail": :test, 26 | "coveralls.post": :test, 27 | ], 28 | ] end 29 | 30 | def application do 31 | [applications: [:logger]] 32 | end 33 | 34 | defp description do 35 | """ 36 | Monadic, softly-typed, extensible effect handling in Elixir. 37 | """ 38 | end 39 | 40 | defp package do [ 41 | name: :effects, 42 | files: ["lib", "mix.exs", "README*"], 43 | maintainers: ["Izaak Schroeder"], 44 | licenses: ["CC0-1.0"], 45 | links: %{"GitHub" => "https://github.com/metalabdesign/effects"} 46 | ] end 47 | 48 | defp aliases do [ 49 | lint: ["dogma"], 50 | test: ["coveralls"], 51 | ] end 52 | 53 | defp deps do [ 54 | # Test coverage 55 | {:excoveralls, "~> 0.4", only: [:dev, :test]}, 56 | # Static analysis 57 | {:dialyxir, "~> 0.3", only: [:dev, :test]}, 58 | # Test-style 59 | {:espec, "~> 0.8.17", only: [:dev, :test]}, 60 | # Linting 61 | {:dogma, "~> 0.1.4", only: [:dev, :test]}, 62 | # Documentation generation 63 | {:ex_doc, "~> 0.13.0", only: [:dev]}, 64 | ] end 65 | end 66 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"certifi": {:hex, :certifi, "0.4.0", "a7966efb868b179023618d29a407548f70c52466bf1849b9e8ebd0e34b7ea11f", [:rebar3], []}, 2 | "dialyxir": {:hex, :dialyxir, "0.3.3", "2f8bb8ab4e17acf4086cae847bd385c0f89296d3e3448dc304c26bfbe4b46cb4", [:mix], []}, 3 | "dogma": {:hex, :dogma, "0.1.4", "37b96934161f81838e6d461d22a7d92a5ffcf9a90b21e38845c2f6bf77770562", [:mix], [{:poison, ">= 1.0.0", [hex: :poison, optional: false]}]}, 4 | "earmark": {:hex, :earmark, "1.0.1", "2c2cd903bfdc3de3f189bd9a8d4569a075b88a8981ded9a0d95672f6e2b63141", [:mix], []}, 5 | "espec": {:hex, :espec, "0.8.18", "63362f6f51531bd8c03b23cb9724ce4550617307c0561930dfa07cb4f40d922e", [:mix], [{:meck, "~> 0.8.4", [hex: :meck, optional: false]}]}, 6 | "ex_doc": {:hex, :ex_doc, "0.13.0", "aa2f8fe4c6136a2f7cfc0a7e06805f82530e91df00e2bff4b4362002b43ada65", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}, 7 | "excoveralls": {:hex, :excoveralls, "0.5.2", "05c5cd667c311b252807506e5bd5861f8ec4627289b275994a30379edbc82d7f", [:mix], [{:exjsx, "~> 3.0", [hex: :exjsx, optional: false]}, {:hackney, ">= 0.12.0", [hex: :hackney, optional: false]}]}, 8 | "exjsx": {:hex, :exjsx, "3.2.0", "7136cc739ace295fc74c378f33699e5145bead4fdc1b4799822d0287489136fb", [:mix], [{:jsx, "~> 2.6.2", [hex: :jsx, optional: false]}]}, 9 | "hackney": {:hex, :hackney, "1.6.0", "8d1e9440c9edf23bf5e5e2fe0c71de03eb265103b72901337394c840eec679ac", [:rebar3], [{:certifi, "0.4.0", [hex: :certifi, optional: false]}, {:idna, "1.2.0", [hex: :idna, optional: false]}, {:metrics, "1.0.1", [hex: :metrics, optional: false]}, {:mimerl, "1.0.2", [hex: :mimerl, optional: false]}, {:ssl_verify_fun, "1.1.0", [hex: :ssl_verify_fun, optional: false]}]}, 10 | "idna": {:hex, :idna, "1.2.0", "ac62ee99da068f43c50dc69acf700e03a62a348360126260e87f2b54eced86b2", [:rebar3], []}, 11 | "jsx": {:hex, :jsx, "2.6.2", "213721e058da0587a4bce3cc8a00ff6684ced229c8f9223245c6ff2c88fbaa5a", [:mix, :rebar], []}, 12 | "meck": {:hex, :meck, "0.8.4", "59ca1cd971372aa223138efcf9b29475bde299e1953046a0c727184790ab1520", [:rebar, :make], []}, 13 | "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], []}, 14 | "mimerl": {:hex, :mimerl, "1.0.2", "993f9b0e084083405ed8252b99460c4f0563e41729ab42d9074fd5e52439be88", [:rebar3], []}, 15 | "poison": {:hex, :poison, "2.1.0", "f583218ced822675e484648fa26c933d621373f01c6c76bd00005d7bd4b82e27", [:mix], []}, 16 | "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.0", "edee20847c42e379bf91261db474ffbe373f8acb56e9079acb6038d4e0bf414f", [:rebar, :make], []}} 17 | -------------------------------------------------------------------------------- /test/spec/effect.spec.exs: -------------------------------------------------------------------------------- 1 | defmodule Test.Effects do 2 | use ESpec 3 | 4 | import Effects 5 | alias Effects.Pure 6 | alias Effects.Effect 7 | 8 | defp interp(_, %Pure{value: value}) do 9 | value 10 | end 11 | defp interp(value, %Effect{effect: "INC", next: next}) do 12 | interp(value + 1, queue_apply(next, value + 1)) 13 | end 14 | defp interp(value, %Effect{effect: "FXN", next: next}) do 15 | interp(value, queue_apply(next, fn x -> value + x * 2 end)) 16 | end 17 | 18 | # defp anterp({_, state}, %Pure{value: value}) do 19 | # {value, state} 20 | # end 21 | # defp anterp({value,{x,y}} %Effect{effect: "INCX", next: next}) do 22 | # result = Task.async(fn -> Task.await(x) + 1 end) 23 | # interp({value, {result, y}}, queue_apply(next, result)) 24 | # end 25 | # defp anterp(value, %Effect{effect: "INCY", next: next}) do 26 | # result = Task.async(fn -> value + 1 end) 27 | # interp(value + 1, queue_apply(next, value + 1)) 28 | # end 29 | 30 | defeffect inc, do: "INC" 31 | defeffect fxn, do: "FXN" 32 | defeffect dex, do: "TEST" 33 | 34 | describe "pure" do 35 | it "should create new pure object" do 36 | expect pure(5) |> to(eq pure(5)) 37 | end 38 | it "should run through a stateless interpreter" do 39 | expect interp(4, pure(5)) |> to(eq 5) 40 | end 41 | end 42 | 43 | describe "effect" do 44 | it "should create a new effect object" do 45 | expect effect(nil, "INC", nil) |> to(be_struct Effect) 46 | end 47 | end 48 | 49 | describe "fmap" do 50 | it "should work with pure values" do 51 | expect pure(5) |> fmap(fn x -> (x+1) end) 52 | |> to(eq pure(6)) 53 | end 54 | it "should work with effect values" do 55 | expect 2 |> interp(inc |> fmap(fn x -> x * 2 end)) 56 | |> to(eq 6) 57 | end 58 | end 59 | 60 | describe "ap" do 61 | it "should work with two pure values" do 62 | expect (pure fn x-> (x+1) end) |> ap(pure(5)) 63 | |> to(eq pure(6)) 64 | end 65 | 66 | it "should work with pure/effect" do 67 | expect 3 |> interp((pure fn x-> (x * 2) end) |> ap(inc)) 68 | |> to(eq 8) 69 | end 70 | 71 | it "should work with effect/pure" do 72 | expect 3 |> interp(fxn |> ap(pure(4))) 73 | |> to(eq 11) 74 | end 75 | 76 | it "should work with two effect values" do 77 | expect 3 |> interp(fxn |> ap(inc)) 78 | |> to(eq 11) 79 | end 80 | 81 | it "should work with fn/free" do 82 | expect (fn x-> (x+1) end) |> ap(pure(5)) 83 | |> to(eq pure(6)) 84 | end 85 | 86 | it "should work with multiple arguments" do 87 | expect (fn x,y -> (x+y) end) |> ap(pure(5)) |> ap(pure(3)) 88 | |> to(eq pure(8)) 89 | end 90 | 91 | # it "should be executable in parallel" do 92 | # # TODO: Implement me! 93 | # fn (x,y,z) -> x*y*z end <<~ inc <<~ inc <<~ inc 94 | # end 95 | end 96 | 97 | describe "bind" do 98 | it "should work with pure values" do 99 | expect pure(5) |> bind(fn x -> pure x+1 end) 100 | |> to(eq pure(6)) 101 | end 102 | it "should work with effect values" do 103 | expect 3 |> interp(inc |> bind(fn x -> pure(x*2) end)) 104 | |> to(eq 8) 105 | end 106 | end 107 | 108 | describe "~>>" do 109 | it "should work like `bind`" do 110 | expect pure(5) ~>> fn x -> pure x+1 end 111 | |> to(eq pure(6)) 112 | end 113 | end 114 | 115 | describe "~>" do 116 | it "should work" do 117 | expect pure(5) ~> pure(6) |> to(eq pure(6)) 118 | end 119 | end 120 | 121 | describe "<<~" do 122 | it "should work like `apply`" do 123 | expect fn x -> (x+1) end <<~ pure(5) 124 | |> to(eq pure(6)) 125 | end 126 | it "should work like `apply` for multiple arguments" do 127 | expect fn x,y -> (x+y) end <<~ pure(5) <<~ pure(1) 128 | |> to(eq pure(6)) 129 | end 130 | end 131 | 132 | describe "defeffect" do 133 | it "should return a new effect" do 134 | expect dex |> to(be_struct Effect) 135 | end 136 | end 137 | 138 | describe "monad laws" do 139 | it "should obey the left identity" do 140 | f = fn x -> pure(x + 1) end 141 | a = 1 142 | expect (pure(a) ~>> f) |> to(eq f.(a)) 143 | end 144 | it "should obey the right identity" do 145 | m = pure(1) 146 | expect (m ~>> (&pure/1)) |> to(eq m) 147 | end 148 | it "should be associative" do 149 | f = fn x -> pure(x + 1) end 150 | g = fn x -> pure(x * 2) end 151 | m = pure(2) 152 | expect ((m ~>> f) ~>> g) |> to(eq m ~>> &(f.(&1) ~>> g)) 153 | end 154 | end 155 | 156 | describe "interpreter" do 157 | it "should handle sequenced effects" do 158 | effect = inc ~> inc ~> inc 159 | expect 3 |> interp(effect) |> to(eq 6) 160 | end 161 | end 162 | 163 | end 164 | -------------------------------------------------------------------------------- /test/spec/queue.spec.exs: -------------------------------------------------------------------------------- 1 | defmodule Test.Effects.Queue do 2 | use ESpec 3 | 4 | alias Effects.Queue, as: Q 5 | 6 | describe "value" do 7 | it "should work" do 8 | Q.value(5) 9 | |> Q.to_list 10 | |> to(eq [5]) 11 | end 12 | end 13 | 14 | describe "concat" do 15 | it "should work" do 16 | expect Q.concat(Q.value(5), Q.value(6)) 17 | |> Q.to_list 18 | |> to(eq [5, 6]) 19 | end 20 | end 21 | 22 | describe "append" do 23 | it "should work" do 24 | expect Q.value(5) 25 | |> Q.append(6) 26 | |> Q.to_list 27 | |> to(eq [5, 6]) 28 | end 29 | end 30 | 31 | describe "pop" do 32 | it "should work in simple cases" do 33 | {value} = Q.value(5) 34 | |> Q.pop 35 | 36 | expect value |> to(eq 5) 37 | end 38 | 39 | it "should work in complex cases" do 40 | {_, next} = Q.value(5) 41 | |> Q.append(6) 42 | |> Q.append(7) 43 | |> Q.pop 44 | 45 | {value, _} = next 46 | |> Q.append(7) 47 | |> Q.pop() 48 | 49 | expect value |> to(eq 6) 50 | end 51 | end 52 | 53 | end 54 | -------------------------------------------------------------------------------- /test/spec/spec_helper.exs: -------------------------------------------------------------------------------- 1 | ESpec.configure fn(config) -> 2 | config.before fn -> 3 | {:shared, hello: :world} 4 | end 5 | 6 | config.finally fn(_shared) -> 7 | :ok 8 | end 9 | end 10 | --------------------------------------------------------------------------------