dry-effects 0.1.2 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +57 -5
- data/LICENSE +1 -1
- data/README.md +19 -11
- data/dry-effects.gemspec +33 -41
- data/lib/dry/effects/all.rb +4 -4
- data/lib/dry/effects/container.rb +1 -1
- data/lib/dry/effects/effect.rb +2 -2
- data/lib/dry/effects/effects/async.rb +3 -1
- data/lib/dry/effects/effects/cache.rb +13 -9
- data/lib/dry/effects/effects/cmp.rb +3 -1
- data/lib/dry/effects/effects/current_time.rb +4 -2
- data/lib/dry/effects/effects/defer.rb +3 -1
- data/lib/dry/effects/effects/env.rb +4 -2
- data/lib/dry/effects/effects/fork.rb +3 -1
- data/lib/dry/effects/effects/implicit.rb +4 -2
- data/lib/dry/effects/effects/interrupt.rb +4 -2
- data/lib/dry/effects/effects/lock.rb +8 -6
- data/lib/dry/effects/effects/parallel.rb +4 -2
- data/lib/dry/effects/effects/random.rb +5 -3
- data/lib/dry/effects/effects/reader.rb +1 -1
- data/lib/dry/effects/effects/resolve.rb +23 -3
- data/lib/dry/effects/effects/retry.rb +4 -2
- data/lib/dry/effects/effects/state.rb +4 -2
- data/lib/dry/effects/effects/timeout.rb +3 -1
- data/lib/dry/effects/effects/timestamp.rb +3 -1
- data/lib/dry/effects/errors.rb +4 -4
- data/lib/dry/effects/extensions/active_support/tagged_logging.rb +13 -0
- data/lib/dry/effects/extensions/auto_inject.rb +5 -5
- data/lib/dry/effects/extensions/system.rb +8 -7
- data/lib/dry/effects/extensions.rb +12 -4
- data/lib/dry/effects/frame.rb +30 -9
- data/lib/dry/effects/halt.rb +3 -3
- data/lib/dry/effects/handler.rb +1 -1
- data/lib/dry/effects/inflector.rb +1 -1
- data/lib/dry/effects/initializer.rb +17 -16
- data/lib/dry/effects/instruction.rb +1 -1
- data/lib/dry/effects/instructions/execute.rb +2 -1
- data/lib/dry/effects/instructions/raise.rb +2 -1
- data/lib/dry/effects/provider/class_interface.rb +2 -2
- data/lib/dry/effects/provider.rb +2 -2
- data/lib/dry/effects/providers/async.rb +2 -2
- data/lib/dry/effects/providers/cache.rb +2 -2
- data/lib/dry/effects/providers/cmp.rb +1 -1
- data/lib/dry/effects/providers/current_time/time_generators.rb +1 -1
- data/lib/dry/effects/providers/current_time.rb +5 -5
- data/lib/dry/effects/providers/defer.rb +6 -6
- data/lib/dry/effects/providers/env.rb +2 -2
- data/lib/dry/effects/providers/fork.rb +2 -2
- data/lib/dry/effects/providers/implicit.rb +1 -1
- data/lib/dry/effects/providers/interrupt.rb +3 -3
- data/lib/dry/effects/providers/lock.rb +6 -8
- data/lib/dry/effects/providers/parallel.rb +3 -3
- data/lib/dry/effects/providers/random.rb +74 -2
- data/lib/dry/effects/providers/reader.rb +1 -1
- data/lib/dry/effects/providers/resolve.rb +8 -7
- data/lib/dry/effects/providers/retry.rb +5 -7
- data/lib/dry/effects/providers/state.rb +2 -2
- data/lib/dry/effects/providers/timeout.rb +2 -2
- data/lib/dry/effects/providers/timestamp.rb +2 -2
- data/lib/dry/effects/stack.rb +6 -6
- data/lib/dry/effects/version.rb +1 -1
- data/lib/dry/effects.rb +7 -7
- metadata +22 -69
- data/.codeclimate.yml +0 -12
- data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +0 -10
- data/.github/ISSUE_TEMPLATE/---bug-report.md +0 -30
- data/.github/ISSUE_TEMPLATE/---feature-request.md +0 -18
- data/.github/workflows/ci.yml +0 -74
- data/.github/workflows/docsite.yml +0 -34
- data/.github/workflows/sync_configs.yml +0 -34
- data/.gitignore +0 -12
- data/.rspec +0 -4
- data/.rubocop.yml +0 -95
- data/CODE_OF_CONDUCT.md +0 -13
- data/CONTRIBUTING.md +0 -29
- data/Gemfile +0 -23
- data/Rakefile +0 -8
- data/docsite/source/effects/cache.html.md +0 -84
- data/docsite/source/effects/current_time.html.md +0 -186
- data/docsite/source/effects/defer.html.md +0 -130
- data/docsite/source/effects/env.html.md +0 -144
- data/docsite/source/effects/interrupt.html.md +0 -109
- data/docsite/source/effects/parallel.html.md +0 -25
- data/docsite/source/effects/reader.html.md +0 -126
- data/docsite/source/effects/resolve.html.md +0 -188
- data/docsite/source/effects/state.html.md +0 -178
- data/docsite/source/effects/timeout.html.md +0 -42
- data/docsite/source/effects.html.md +0 -29
- data/docsite/source/index.html.md +0 -212
- data/examples/cmp.rb +0 -51
- data/examples/state.rb +0 -29
@@ -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,42 +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
|
-
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.
|
@@ -1,29 +0,0 @@
|
|
1
|
-
---
|
2
|
-
title: Effects
|
3
|
-
layout: gem-single
|
4
|
-
name: dry-effects
|
5
|
-
sections:
|
6
|
-
- reader
|
7
|
-
- state
|
8
|
-
- resolve
|
9
|
-
- cache
|
10
|
-
- current_time
|
11
|
-
- env
|
12
|
-
- interrupt
|
13
|
-
- defer
|
14
|
-
- timeout
|
15
|
-
- parallel
|
16
|
-
---
|
17
|
-
|
18
|
-
`dry-effects` has a bunch of built-in effects and providers:
|
19
|
-
|
20
|
-
- [Reader](/gems/dry-effects/0.1/effects/reader)
|
21
|
-
- [State](/gems/dry-effects/0.1/effects/state)
|
22
|
-
- [Resolve (Dependency Injection)](/gems/dry-effects/0.1/effects/resolve)
|
23
|
-
- [Cache](/gems/dry-effects/0.1/effects/cache)
|
24
|
-
- [Current Time](/gems/dry-effects/0.1/effects/current_time)
|
25
|
-
- [Environment](/gems/dry-effects/0.1/effects/env)
|
26
|
-
- [Interrupt](/gems/dry-effects/0.1/effects/interrupt)
|
27
|
-
- [Deferred Execution](/gems/dry-effects/0.1/effects/defer)
|
28
|
-
- [Timeout](/gems/dry-effects/0.1/effects/timeout)
|
29
|
-
- [Parallel execution](/gems/dry-effects/0.1/effects/parallel)
|
@@ -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).
|