dry-effects 0.1.4 → 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,188 +0,0 @@
1
- ---
2
- title: Resolve (Dependency Injection)
3
- layout: gem-single
4
- name: dry-effects
5
- ---
6
-
7
- Resolve is an effect for injecting dependencies. A simple usage example:
8
-
9
- ```ruby
10
- require 'dry/effects'
11
-
12
- class CreateUser
13
- include Dry::Effects.Resolve(:user_repo)
14
-
15
- def call(values)
16
- name = values.values_at(:first_name, :last_name).join(' ')
17
- user_repo.create(values.merge(name: name))
18
- end
19
- end
20
- ```
21
-
22
- Providing `user_repo` in tests:
23
-
24
- ```ruby
25
- RSpec.describe CreateUser do
26
- # adds #provide
27
- include Dry::Effects::Handler.Resolve
28
-
29
- subject(:create_user) { described_class.new }
30
-
31
- let(:user_repo) { double(:user_repo) }
32
-
33
- it 'creates a user' do
34
- expect(user_repo).to receive(:create).with(
35
- first_name: 'John',
36
- last_name: 'Doe',
37
- name: 'John Doe'
38
- )
39
-
40
- provide(user_repo: user_repo) { create_user.(first_name: 'John', last_name: 'Doe') }
41
- end
42
- end
43
- ```
44
-
45
- Providing dependencies with middleware:
46
-
47
- ```ruby
48
- class ProviderMiddleware
49
- include Dry::Effects::Handler.Resolve
50
-
51
- def initialize(app, dependencies)
52
- @app = app
53
- @dependencies = dependencies
54
- end
55
-
56
- def call(env)
57
- provide(@dependencies) { @app.(env) }
58
- end
59
- end
60
- ```
61
-
62
- Then in `config.ru`:
63
-
64
- ```ruby
65
- # ...some bootstrapping code ...
66
-
67
- use ProviderMiddleware, user_repo: UserRepo.new
68
- run Application.new
69
- ```
70
-
71
- ### Compatibility with `dry-container` and `dry-system`
72
-
73
- Any object that responds to `.key?` and `.[]` can be used for providing dependencies. Thus, the default Resolve provider is compatible with `dry-container` and `dry-system` out of the box.
74
-
75
- ```ruby
76
- def call(env)
77
- # Assuming App is a subclass of Dry::System::Container
78
- provide(App) { @app.(env) }
79
- end
80
- ```
81
-
82
- ### Providing static values
83
-
84
- One can pass a container to the module builder:
85
-
86
- ```ruby
87
- class ProviderMiddleware
88
- include Dry::Effects::Handler.Resolve(Application)
89
-
90
- def initialize(app)
91
- @app = app
92
- end
93
-
94
- def call(env)
95
- # Here Application will be used for resolving dependencies
96
- provide { @app.(env) }
97
- end
98
- end
99
- ```
100
-
101
- ### Injecting many keys and using aliases
102
-
103
- ```ruby
104
- require 'dry/effects'
105
-
106
- class CreateUser
107
- include Dry::Effects.Resolve(
108
- # Injected as .schema
109
- # but resolved with 'operations.create_user.schema'
110
- 'operations.create_user.schema',
111
- # Injected as .repo
112
- # but resolved with 'repos.user_repo'
113
- repo: 'repos.user_repo'
114
- )
115
-
116
- def call(values)
117
- result = schema.(values)
118
-
119
- if result.success?
120
- user = repo.create(result.to_h)
121
- [:ok, user]
122
- else
123
- [:err, result]
124
- end
125
- end
126
- end
127
- ```
128
-
129
- ### Overriding dependencies in test environment
130
-
131
- Sometimes you may want to push dependencies through an existing handler. This is normally needed for testing when you want to replace some dependencies in a test environment for an assembled app, like a Rack application. Passing `overridable: true` enables it:
132
-
133
- ```ruby
134
- require 'dry/effects'
135
-
136
- class ProviderMiddleware
137
- include Dry::Effects::Handler.Resolve
138
-
139
- def initialize(app)
140
- @app = app
141
- end
142
-
143
- def call(env)
144
- provide(Application, overridable: overridable?) { @app.(env) }
145
- end
146
-
147
- def overridable?
148
- ENV['RACK_ENV'].eql?('test')
149
- end
150
- end
151
- ```
152
-
153
- Now in tests, you can override some dependencies at will:
154
-
155
- ```ruby
156
- require 'dry/effects'
157
- require 'rack/test'
158
-
159
- RSpec.describe do
160
- include Rack::Test::Methods
161
- include Dry::Effects::Handler.Provider
162
-
163
- let(:app) do
164
- # building an assembled rack app
165
- end
166
-
167
- describe 'POST /users' do
168
- let(:user_repo) { double(:user_repo) }
169
-
170
- it 'creates a user' do
171
- expect(user_repo).to receive(:create).with(
172
- first_name: 'John', last_name: 'Doe'
173
- ).and_return(1)
174
-
175
- # Overriding one dependency
176
- # It will only work if `overridable: true` is passed
177
- # in the middleware
178
- provide('repos.user_repo' => user_repo) do
179
- post(
180
- '/users',
181
- JSON.dump(first_name: 'John', last_name: 'Doe'),
182
- 'CONTENT_TYPE' => 'application/json'
183
- )
184
- end
185
- end
186
- end
187
- end
188
- ```
@@ -1,178 +0,0 @@
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.
@@ -1,44 +0,0 @@
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
- include Dry::Effects::Handler.Timeout(:http)
31
-
32
- def initialize(app)
33
- @app = app
34
- end
35
-
36
- def call(env)
37
- with_timeout(10.0) { @app.(env) }
38
- rescue Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout
39
- [504, {}, ["Gateway Timeout"]]
40
- end
41
- end
42
- ```
43
-
44
- 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.
@@ -1,212 +0,0 @@
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).