dry-effects 0.1.0 → 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,178 @@
1
+ ---
2
+ title: State
3
+ layout: gem-single
4
+ name: dry-effects
5
+ ---
6
+
7
+ State is a mutation effect. It allows [reading](/gems/dry-effects/0.1/effects/reader) and _writing_ non-local values.
8
+
9
+ ### Basic usage
10
+
11
+ Handling code:
12
+
13
+ ```ruby
14
+ require 'dry/effects'
15
+
16
+ class CountCalls
17
+ include Dry::Effects::Handler.State(:counter)
18
+
19
+ def call
20
+ counter, result = with_counter(0) do
21
+ yield
22
+ end
23
+
24
+ puts "Counter: #{counter}"
25
+
26
+ result
27
+ end
28
+ end
29
+ ```
30
+
31
+ Using code:
32
+
33
+ ```ruby
34
+ require 'dry/effects'
35
+
36
+ class HeavyLifting
37
+ include Dry::Effects.State(:counter)
38
+
39
+ def call
40
+ self.counter += 1
41
+ # ... do heavy work ...
42
+ end
43
+ end
44
+ ```
45
+
46
+ Now it's simple to count calls by gluing two pieces:
47
+
48
+ ```ruby
49
+ count_calls = CountCalls.new
50
+ heavy_lifting = HeavyLifting.new
51
+
52
+ count_calls.() { 1000.times { heavy_lifting.() }; :done }
53
+ # Counter: 1000
54
+ # => :done
55
+ ```
56
+
57
+ ### Handler interface
58
+
59
+ As shown above, the State handler returns two values: the accumulated state and the return value of the block:
60
+
61
+ ```ruby
62
+ include Dry::Effects::Handler.State(:state)
63
+
64
+ def run(&block)
65
+ accumulated_state, result = with_state(initial_state) do
66
+ block.call
67
+ end
68
+
69
+ # result holds the return value of block.call
70
+
71
+ # accumulated_state refers to the last written value
72
+ # or initial_value if the state wasn't changed
73
+ end
74
+ ```
75
+
76
+ ### Identifiers and mixing states
77
+
78
+ All state handlers and effects have an identifier. Effects with different identifiers are compatible without limitations but swapping the handlers may change the result:
79
+
80
+ ```ruby
81
+ require 'dry/effects'
82
+
83
+ class Program
84
+ include Dry::Effects.State(:sum)
85
+ include Dry::Effects.State(:product)
86
+
87
+ def call
88
+ 1.upto(10) do |i|
89
+ self.sum += i
90
+ self.product *= i
91
+ end
92
+
93
+ :done
94
+ end
95
+ end
96
+
97
+ program = Program.new
98
+
99
+ extend Dry::Effects::Handler.State(:sum)
100
+ extend Dry::Effects::Handler.State(:product)
101
+
102
+ with_sum(0) { with_product(1) { program.call } }
103
+ # => [55, [3628800, :done]]
104
+ with_product(1) { with_sum(0) { program.call } }
105
+ # => [3628800, [55, :done]]
106
+ ```
107
+
108
+ ### Relation to Reader
109
+
110
+ A State handler eliminates Reader effects with the same identifier:
111
+
112
+ ```ruby
113
+ require 'dry/effects'
114
+
115
+ extend Dry::Effects::Handler.State(:counter)
116
+ extend Dry::Effects.Reader(:counter)
117
+
118
+ with_counter(100) { "Counter value is #{counter}" }
119
+ # => [100, "Counter values is 100"]
120
+ ```
121
+
122
+ ### Not providing an initial value
123
+
124
+ There are cases when an initial value cannot be provided. You can skip the initial value but in this case, reading it _before_ writing will raise an error:
125
+
126
+ ```ruby
127
+ extend Dry::Effects::Handler.State(:user)
128
+ extend Dry::Effects.State(:user)
129
+
130
+ with_user { user }
131
+ # => Dry::Effects::Errors::UndefinedStateError (+user+ is not defined, you need to assign it first by using a writer, passing initial value to the handler, or providing a fallback value)
132
+
133
+ with_user do
134
+ self.user = 'John'
135
+
136
+ "Hello, #{user}"
137
+ end
138
+ # => ["John", "Hello, John"]
139
+ ```
140
+
141
+ One example is testing middleware without mutating `env`:
142
+
143
+ ```ruby
144
+ RSpec.describe AddingSomeMiddleware do
145
+ include Dry::Effects::Handler.State(:env)
146
+ include Dry::Effects.State(:env)
147
+
148
+ let(:app) do
149
+ lambda do |env|
150
+ self.env = env
151
+ [200, {}, ["ok"]]
152
+ end
153
+ end
154
+
155
+ subject(:middleware) { described_class.new(app) }
156
+
157
+ it 'adds SOME_KEY to env' do
158
+ captured_env, _ = middleware.({})
159
+
160
+ expect(captured_env).to have_key('SOME_KEY')
161
+ end
162
+ end
163
+ ```
164
+
165
+ ### Default value for Reader effects
166
+
167
+ When no initial value is given, you can use a block for providing a default value:
168
+
169
+ ```ruby
170
+ extend Dry::Effects::Handler.State(:artist)
171
+ extend Dry::Effects.State(:artist)
172
+
173
+ with_artist { artist { 'Unknown Artist' } } # => "Unknown Artist"
174
+ ```
175
+
176
+ ### When to use?
177
+
178
+ State is a classic example of an effect. However, using it often can make your code harder to follow.
@@ -0,0 +1,42 @@
1
+ ---
2
+ title: Timeout
3
+ layout: gem-single
4
+ name: dry-effects
5
+ ---
6
+
7
+ `Timeout` consists of two methods:
8
+
9
+ - `timeout` returns an ever-decreasing number of seconds until this number reaches 0.
10
+ - `timed_out?` checks if no time left.
11
+
12
+ The handler provides the initial timeout and uses the monotonic time for counting down.
13
+
14
+ A practical example is limiting the length of all external HTTP calls during request processing. Sample class for making HTTP requests in an application:
15
+
16
+ ```ruby
17
+ class MakeRequest
18
+ include Dry::Effects.Timeout(:http)
19
+
20
+ def call(url)
21
+ HTTParty.get(url, timeout: timeout)
22
+ end
23
+ end
24
+ ```
25
+
26
+ Handling timeouts:
27
+
28
+ ```ruby
29
+ class WithTimeout
30
+ def initialize(app)
31
+ @app = app
32
+ end
33
+
34
+ def call(env)
35
+ with_timeout(10.0) { @app.(env) }
36
+ rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout
37
+ [504, {}, ["Gateway Timeout"]]
38
+ end
39
+ end
40
+ ```
41
+
42
+ The code above guarantees all requests made with `MakeRequest` during `@app.(env)` will finish within 10 seconds. If `@app` doesn't spend much time somewhere else, it gives a reasonably reliable hard limit on request processing.
@@ -0,0 +1,212 @@
1
+ ---
2
+ title: Introduction
3
+ layout: gem-single
4
+ type: gem
5
+ name: dry-effects
6
+ sections:
7
+ - effects
8
+ ---
9
+
10
+ `dry-effects` is a practical, production-oriented implementation of algebraic effects in Ruby.
11
+
12
+ ### Why?
13
+
14
+ Algebraic effects are a powerful tool for writing composable and testable code in a safe way. Fundamentally, any effect consists of two parts: introduction (throwing effect) and elimination (handling effect with an _effect provider_). One of the many things you can do with them is sharing state:
15
+
16
+ ```ruby
17
+ require 'dry/effects'
18
+
19
+ class CounterMiddleware
20
+ # This adds a `counter` effect provider. It will handle (eliminate) effects
21
+ include Dry::Effects::Handler.State(:counter)
22
+
23
+ def initialize(app)
24
+ @app = app
25
+ end
26
+
27
+ def call(env)
28
+ # Calling `with_counter` makes the value available anywhere in `@app.call`
29
+ counter, response = with_counter(0) do
30
+ @app.(env)
31
+ end
32
+
33
+ # Once processing is complete, the result value
34
+ # will be stored in `counter`
35
+
36
+ response
37
+ end
38
+ end
39
+
40
+ ### Somewhere deep in your app
41
+
42
+ class CreatePost
43
+ # Adds counter accessor (by introducing state effects)
44
+ include Dry::Effects.State(:counter)
45
+
46
+ def call(values)
47
+ # Value is passed from middleware
48
+ self.counter += 1
49
+ # ...
50
+ end
51
+ end
52
+ ```
53
+
54
+ `CreatePost#call` can only be called when there's `with_counter` somewhere in the stack. If you want to test `CreatePost` separately, you'll need to use `with_counter` in tests too:
55
+
56
+ ```ruby
57
+ require 'dry/effects'
58
+ require 'posting_app/create_post'
59
+
60
+ RSpec.describe CreatePost do
61
+ include Dry::Effects::Handler::State(:counter)
62
+
63
+ subject(:create_post) { described_class.new }
64
+
65
+ it 'updates the counter' do
66
+ counter, post = with_counter(0) { create_post.(post_values) }
67
+
68
+ expect(counter).to be(1)
69
+ end
70
+ end
71
+ ```
72
+
73
+ Any introduced effect must have a handler. If no handler found you'll see an error:
74
+
75
+ ```ruby
76
+ CreatePost.new.({})
77
+ # => Dry::Effects::Errors::MissingStateError (Value of +counter+ is not set, you need to provide value with an effect handler)
78
+ ```
79
+
80
+ In a statically typed with support for algebraic effects you won't be able to run code without providing all required handlers, it'd be a type error.
81
+
82
+ It may remind you using global state, but it's not actually global. It should instead be called "goto on steroids" or "goto made unharmful."
83
+
84
+ ### Cmp
85
+
86
+ State sharing is one of many effects already supported; another example is comparative execution. Imagine you test a new feature that ideally shouldn't affect application responses.
87
+
88
+ ```ruby
89
+ require 'dry/effects'
90
+
91
+ class TestNewFeatureMiddleware
92
+ # `as:` renames handler method
93
+ include Dry::Effects::Handler.Cmp(:feature, as: :test_feature)
94
+
95
+ def initialize(app)
96
+ @app = app
97
+ end
98
+
99
+ def call(env)
100
+ without_feature, with_feature = test_feature do
101
+ @app.(env)
102
+ end
103
+
104
+ if with_feature != without_feature
105
+ # something is different!
106
+ end
107
+
108
+ without_feature
109
+ end
110
+ end
111
+
112
+ ### Somewhere deep in your app
113
+
114
+ class PostView
115
+ include Dry::Effects.Cmp(:feature)
116
+
117
+ def call
118
+ if feature?
119
+ # do render with feature
120
+ else
121
+ # do render without feature
122
+ end
123
+ end
124
+ end
125
+ ```
126
+
127
+ The `Cmp` provider will run your code twice so that you can compare the results and detect differences.
128
+
129
+ ### Composition
130
+
131
+ So far effects haven't shown anything algebraic about themselves. Here comes composition. Any effect is composable with one another. Say we have code using both `State` and `Cmp` effects:
132
+
133
+ ```ruby
134
+ require 'dry/effects'
135
+
136
+ class GreetUser
137
+ include Dry::Effects.Cmp(:excitement)
138
+ include Dry::Effects.State(:greetings_given)
139
+
140
+ def call(name)
141
+ self.greetings_given += 1
142
+
143
+ if excitement?
144
+ "#{greetings_given}. Hello #{name}!"
145
+ else
146
+ "#{greetings_given}. Hello #{name}"
147
+ end
148
+ end
149
+ end
150
+ ```
151
+
152
+ It's a simple piece of code that requires a single argument and two effect handlers to run:
153
+
154
+ ```ruby
155
+ class Context
156
+ include Dry::Effects::Handler.Cmp(:excitement, as: :test_excitement)
157
+ include Dry::Effects::Handler.State(:greetings_given)
158
+
159
+ def initialize
160
+ @greeting = GreetUser.new
161
+ end
162
+
163
+ def call(name)
164
+ test_excitement do
165
+ with_greetings_given(0) do
166
+ @greeting.(name)
167
+ end
168
+ end
169
+ end
170
+ end
171
+
172
+ Context.new.('Alice')
173
+ # => [[1, "1. Hello Alice"], [1, "1. Hello Alice!"]]
174
+ ```
175
+
176
+ The result is two branches with `excitement=false` and `excitement=true`. Every variant has its state handler and hence returns another array with the number of greetings given and the greeting. However, neither our code nor algebraic effects restrict the order in which the effects are meant to be handled so let's swap the handlers:
177
+
178
+ ```ruby
179
+ class Context
180
+ # ...
181
+ def call(name)
182
+ with_greetings_given(0) do
183
+ test_excitement do
184
+ @greeting.(name)
185
+ end
186
+ end
187
+ end
188
+ end
189
+
190
+ Context.new.('Alice')
191
+ # => [2, ["1. Hello Alice", "2. Hello Alice!"]]
192
+ ```
193
+
194
+ Now the same code returns a different result! Even more, it has a different shape (or type, if you will): `((Integer, String), (Integer, String))` vs. `(Integer, (String, String))`!
195
+
196
+ ### Algebraic effects
197
+
198
+ Algebraic effects are relatively recent research describing a possible implementation of the effect system. An effect is some capability your code requires to be executed. It gives control over what your code does and helps a lot with testing without involving any magic like `allow(Time).to receive(:now).and_return(@time_now)`. Instead, getting the current time is just another effect, as simple as that.
199
+
200
+ Algebraic effects lean towards functional programming enabling things like dependency injection, mutable state, obtaining the current time and random values in pure code. All that is done avoiding troubles accompanying monad stacks and monad transformers. Even things like JavaScript's `async`/`await` and Python's `asyncio` can be generalized with algebraic effects.
201
+
202
+ If you're interested in the subject, there is a list of articles, papers, and videos, in no particular order:
203
+
204
+ - [Algebraic Effects for the Rest of Us](https://overreacted.io/algebraic-effects-for-the-rest-of-us/) by Dan Abramov, an (unsophisticated) introduction for React/JavaScript developers.
205
+ - [An Introduction to Algebraic Effects and Handlers](https://www.eff-lang.org/handlers-tutorial.pdf) is an approachable paper describing the semantics. Take a look if you want to know more on the subject.
206
+ - [Algebraic Effects for Functional Programming](https://www.microsoft.com/en-us/research/wp-content/uploads/2016/08/algeff-tr-2016-v2.pdf) is another paper by Microsoft Research.
207
+ - [Asynchrony with Algebraic Effects](https://www.youtube.com/watch?v=hrBq8R_kxI0) intro given by Daan Leijen, the author of the previous paper and the [Koka](https://github.com/koka-lang/koka) programming language created specifically for exploring algebraic effects.
208
+ - [Do Be Do Be Do](https://arxiv.org/pdf/1611.09259.pdf) describes the Frank programming language with typed effects and ML-like syntax.
209
+
210
+ ### Goal of dry-effects
211
+
212
+ Despite different effects are compatible one with each other, libraries implementing them (not using them!) are not compatible out of the box. `dry-effects` is aimed to be the standard implementation across dry-rb and rom-rb gems (and possibly others).
@@ -11,3 +11,7 @@ end
11
11
  Dry::Effects.register_extension(:system) do
12
12
  require 'dry/effects/extensions/system'
13
13
  end
14
+
15
+ Dry::Effects.register_extension(:rspec) do
16
+ require 'dry/effects/extensions/rspec'
17
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # These patches make those rspec parts depending on Thread.current
4
+ # play nice with dry-effects.
5
+ # They've been used in production for months before been added here.
6
+
7
+ RSpec::Support.singleton_class.prepend(Module.new {
8
+ include Dry::Effects.Reader(:rspec, as: :effect_local_data)
9
+
10
+ def thread_local_data
11
+ effect_local_data { super }
12
+ end
13
+ })
14
+
15
+ RSpec::Core::Runner.prepend(Module.new {
16
+ include Dry::Effects::Handler.Reader(:rspec, as: :run_with_data)
17
+
18
+ def run_specs(*)
19
+ run_with_data(RSpec::Support.thread_local_data) { super }
20
+ end
21
+ })