affect 0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +50 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +19 -0
- data/LICENSE +21 -0
- data/README.md +300 -0
- data/affect.gemspec +23 -0
- data/examples/fact.rb +23 -0
- data/examples/greet.rb +16 -0
- data/examples/logging.rb +14 -0
- data/examples/pat.rb +22 -0
- data/lib/affect.rb +106 -0
- data/lib/affect/version.rb +5 -0
- data/test/test_affect.rb +133 -0
- metadata +75 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 0ef9c069aa2f0077d7cbf108e9f9a36aaf2a1808fdc1636d37e40333b6cfc2e0
|
4
|
+
data.tar.gz: 54e9f068ed3fecdb8dd624efb41b9540ecb1259b569636fca193a7c0fc27e172
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d2aadb70ccf3ea71c0dfa794cbe33424a4b2ab74ec6208d0137e3664149320a748199dc6c4944dd677ed3f4ace979e6411878165918e4bbd68c539eadbaa02cc
|
7
|
+
data.tar.gz: 983d269425f777d7aae8ce64251b293a170a8dc5dfd7a57a46bc0827e7e81c0bfaadd3111d9e578a15676d0621526278054a17c06e6b7c7bd098f2f8d26dfc66
|
data/.gitignore
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
/.config
|
4
|
+
/coverage/
|
5
|
+
/InstalledFiles
|
6
|
+
/pkg/
|
7
|
+
/spec/reports/
|
8
|
+
/spec/examples.txt
|
9
|
+
/test/tmp/
|
10
|
+
/test/version_tmp/
|
11
|
+
/tmp/
|
12
|
+
|
13
|
+
# Used by dotenv library to load environment variables.
|
14
|
+
# .env
|
15
|
+
|
16
|
+
## Specific to RubyMotion:
|
17
|
+
.dat*
|
18
|
+
.repl_history
|
19
|
+
build/
|
20
|
+
*.bridgesupport
|
21
|
+
build-iPhoneOS/
|
22
|
+
build-iPhoneSimulator/
|
23
|
+
|
24
|
+
## Specific to RubyMotion (use of CocoaPods):
|
25
|
+
#
|
26
|
+
# We recommend against adding the Pods directory to your .gitignore. However
|
27
|
+
# you should judge for yourself, the pros and cons are mentioned at:
|
28
|
+
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
|
29
|
+
#
|
30
|
+
# vendor/Pods/
|
31
|
+
|
32
|
+
## Documentation cache and generated files:
|
33
|
+
/.yardoc/
|
34
|
+
/_yardoc/
|
35
|
+
/doc/
|
36
|
+
/rdoc/
|
37
|
+
|
38
|
+
## Environment normalization:
|
39
|
+
/.bundle/
|
40
|
+
/vendor/bundle
|
41
|
+
/lib/bundler/man/
|
42
|
+
|
43
|
+
# for a library or gem, you might want to ignore these files since the code is
|
44
|
+
# intended to run in multiple environments; otherwise, check them in:
|
45
|
+
# Gemfile.lock
|
46
|
+
# .ruby-version
|
47
|
+
# .ruby-gemset
|
48
|
+
|
49
|
+
# unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
|
50
|
+
.rvmrc
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2019 Sharon Rosner
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,300 @@
|
|
1
|
+
# Affect - structured side effects for functional Ruby
|
2
|
+
|
3
|
+
[INSTALL](#installing-affect) |
|
4
|
+
[TUTORIAL](#getting-started) |
|
5
|
+
[EXAMPLES](examples) |
|
6
|
+
|
7
|
+
> Affect | əˈfɛkt | verb [with object] have an effect on; make a difference to.
|
8
|
+
|
9
|
+
## What is Affect
|
10
|
+
|
11
|
+
Affect is a tiny Ruby gem providing a way to isolate and handle side-effects in
|
12
|
+
functional programs. Affect implements algebraic effects in Ruby, but can also
|
13
|
+
be used to implement patterns that are orthogonal to object-oriented
|
14
|
+
programming, such as inversion of control and dependency injection.
|
15
|
+
|
16
|
+
> **Note**: Affect does not pretend to be a *complete, theoretically correct*
|
17
|
+
> implementation of algebraic effects. Affect concentrates on the idea of
|
18
|
+
> [effect contexts](#the-effect-context). It does not deal with continuations,
|
19
|
+
> asynchrony, or any other concurrency constructs.
|
20
|
+
|
21
|
+
## Installing Affect
|
22
|
+
|
23
|
+
```bash
|
24
|
+
$ gem install affect
|
25
|
+
```
|
26
|
+
|
27
|
+
Or add it to your Gemfile, you know the drill.
|
28
|
+
|
29
|
+
## Getting Started
|
30
|
+
|
31
|
+
Algebraic effects introduces the concept of effect handlers, little pieces of
|
32
|
+
code that are provided by the caller, and invoked by the callee using a uniform
|
33
|
+
interface. An example of algebraic effects might be logging. Normally, if we
|
34
|
+
wanted to log a certain message to `STDOUT` or to a file, we wold do the
|
35
|
+
following:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
def mul(x, y)
|
39
|
+
# assume LOG is a global logger object
|
40
|
+
LOG.info("called with #{x}, #{y}")
|
41
|
+
x * y
|
42
|
+
end
|
43
|
+
|
44
|
+
puts "Result: #{ mul(2, 3) }"
|
45
|
+
```
|
46
|
+
|
47
|
+
The act of logging is a side-effect of our computation. We need to have a global
|
48
|
+
`LOG` object, and we cannot test the functioning of the `mul` method in
|
49
|
+
isolation. What if we wanted to be able to plug-in a custom logger, or intercept
|
50
|
+
calls to the logger?
|
51
|
+
|
52
|
+
Affect provides a solution for such problems by implementing a uniform,
|
53
|
+
composable interface for isolating and handling side effects:
|
54
|
+
|
55
|
+
```ruby
|
56
|
+
require 'affect'
|
57
|
+
|
58
|
+
def mul(x, y)
|
59
|
+
# assume LOG is a global logger object
|
60
|
+
Affect :log, "called with #{x}, #{y}"
|
61
|
+
x * y
|
62
|
+
end
|
63
|
+
|
64
|
+
Affect.wrap {
|
65
|
+
puts "Result: #{ mul(2, 3) }"
|
66
|
+
}.on(:log) { |message|
|
67
|
+
puts "#{Time.now} #{message} (this is a log message)"
|
68
|
+
}.()
|
69
|
+
```
|
70
|
+
|
71
|
+
In the example above, we replace the call to `LOG.info` with an invocation of a
|
72
|
+
`LogIntent` instance. When the intent is passed to `Affect`, the corresponding
|
73
|
+
handler is called in order to perform the intent.
|
74
|
+
|
75
|
+
In essence, by separating the performance of side effects into effect intents,
|
76
|
+
and effect handlers, we have separated the what from the how. The `mul` method
|
77
|
+
is no longer concerned with how to log the message it needs to log. There's no
|
78
|
+
hardbaked reference to a `Log` object, and no logging API to follow. Instead,
|
79
|
+
the *intent* to log a message is passed on to `Affect`, which in turn runs the
|
80
|
+
correct handler that actually does the logging.
|
81
|
+
|
82
|
+
## Performing side effects
|
83
|
+
|
84
|
+
Side effects are performed by calling `Affect.perform` or simply `Affect()` with
|
85
|
+
a specification of the effect to be performed:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
Affect.perform :foo
|
89
|
+
|
90
|
+
# or:
|
91
|
+
Affect :foo
|
92
|
+
```
|
93
|
+
|
94
|
+
You can also pass along more arguments. Those will in turn be passed to the
|
95
|
+
effect handler:
|
96
|
+
|
97
|
+
```ruby
|
98
|
+
Affect :log, 'my message'
|
99
|
+
```
|
100
|
+
|
101
|
+
Effects can be represented using any Ruby object, but in a relatively complex
|
102
|
+
application might be best represented using classes or structs signifying the
|
103
|
+
*intent* to perform an effect:
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
LogIntent = Struct.new(:msg)
|
107
|
+
|
108
|
+
Affect LogIntent.new('my message')
|
109
|
+
```
|
110
|
+
|
111
|
+
When representing effects using symbols, Affect provides a shorthand way to
|
112
|
+
perform effects by calling methods directly on the `Affect` module:
|
113
|
+
|
114
|
+
```ruby
|
115
|
+
Affect.log('my message')
|
116
|
+
```
|
117
|
+
|
118
|
+
Finally, effects should be performed inside of an effect context, by invoking
|
119
|
+
`Affect::Context#call`. Mostly, you'll want to use the callable shorthand
|
120
|
+
`.() { ... }`:
|
121
|
+
|
122
|
+
```ruby
|
123
|
+
def add(x, y)
|
124
|
+
Affect :log, "adding #{x} and #{y}..."
|
125
|
+
x + y
|
126
|
+
end
|
127
|
+
|
128
|
+
Affect.on(:log) { |msg| puts "#{Time.now} #{msg}" }.() {
|
129
|
+
result = add(2, 2)
|
130
|
+
puts "result: #{result}"
|
131
|
+
}
|
132
|
+
```
|
133
|
+
|
134
|
+
## handling side effects
|
135
|
+
|
136
|
+
Side effect handlers can be defined using the `Affect.on` or `Affect.handle`
|
137
|
+
methods. `Affect.on` is used to register one or more effect handlers:
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
# register a single effect handler
|
141
|
+
Affect.on(:log) { |msg| puts "#{Time.now} #{msg}" }
|
142
|
+
|
143
|
+
# register multiple effect handlers by passing in a hash
|
144
|
+
Affect.on(
|
145
|
+
log: ->(msg) { puts "#{Time.now} #{msg}" },
|
146
|
+
ask: -> { gets.chomp }
|
147
|
+
)
|
148
|
+
```
|
149
|
+
|
150
|
+
`Affect.handle` is used as a catch-all handler:
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
Affect.handle do |effect, *args|
|
154
|
+
case effect
|
155
|
+
when :log then puts "#{Time.now} #{msg}"
|
156
|
+
when :ask then gets.chomp
|
157
|
+
end
|
158
|
+
end
|
159
|
+
```
|
160
|
+
|
161
|
+
Note that when `Affect.handle` is used to handle effects, no error will be
|
162
|
+
raised for unhandled effects.
|
163
|
+
|
164
|
+
## The effect context
|
165
|
+
|
166
|
+
Affect defines an effect context which is unique to *each thread or fiber*.
|
167
|
+
Effect contexts can be thought of as stack frames containing information about
|
168
|
+
effect handlers. When effects are invoked using method calls on `Affect`, the
|
169
|
+
call is routed to the most current effect context. If the current effect context
|
170
|
+
does not know how to handle a certain effect, the call will bubble up the stack
|
171
|
+
of effect contexts until a handler is found:
|
172
|
+
|
173
|
+
```ruby
|
174
|
+
# First effect context
|
175
|
+
Affect.on(:log) { |msg| LOG.info(msg) },
|
176
|
+
).() {
|
177
|
+
log("starting")
|
178
|
+
# Second effect context
|
179
|
+
Affect.on(
|
180
|
+
log: ->(*args) { }
|
181
|
+
).() {
|
182
|
+
log("this message will not be logged")
|
183
|
+
}
|
184
|
+
log("stopping")
|
185
|
+
}
|
186
|
+
```
|
187
|
+
|
188
|
+
## Putting it all together
|
189
|
+
|
190
|
+
The Affect API uses method chaining to add effect handlers and finally, execute
|
191
|
+
the application code. Multiple effect handlers can be chained as follows:
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
Affect
|
195
|
+
.on(:ask) { ... }
|
196
|
+
.on(:tell) { ... }
|
197
|
+
.() do
|
198
|
+
...
|
199
|
+
end
|
200
|
+
```
|
201
|
+
|
202
|
+
## Other usages
|
203
|
+
|
204
|
+
### Dependency injection
|
205
|
+
|
206
|
+
Affect can also be used for dependency injection. Dependencies can be injected
|
207
|
+
by providing effect handlers:
|
208
|
+
|
209
|
+
```ruby
|
210
|
+
Affect.on(:db) {
|
211
|
+
get_db_connection
|
212
|
+
}.() {
|
213
|
+
process_users(Affect.db.query('select * from users'))
|
214
|
+
}
|
215
|
+
```
|
216
|
+
|
217
|
+
This is especially useful for testing purposes as described below:
|
218
|
+
|
219
|
+
### Testing
|
220
|
+
|
221
|
+
One particular benefit of using Affect is the way it facilitates testing. When
|
222
|
+
mutable state and side-effects are pulled out of methods and into effect
|
223
|
+
handlers, testing becomes much easier. Side effects can be mocked or tested
|
224
|
+
in isolation, and dependencies provided through effect handlers can also be
|
225
|
+
mocked. The following section includes an example of testing with algebraic
|
226
|
+
effects.
|
227
|
+
|
228
|
+
## Writing applications using algebraic effects
|
229
|
+
|
230
|
+
Algebraic effects have yet to be adopted by any widely used programming
|
231
|
+
language, and they remain a largely theoretical subject in computer science.
|
232
|
+
Their advantages are still to be proven in actual usage. We might discover that
|
233
|
+
they're completely inadequate as a solution for managing side-effects, or we
|
234
|
+
might discover new techniques to be used in conjunction with algebraic effects.
|
235
|
+
|
236
|
+
One important principle to keep in mind is that in order to make the best of
|
237
|
+
algebraic effects, effect handlers need to be pushed to the outside of your
|
238
|
+
code. In most cases, the effect context will be defined in the entry-point of
|
239
|
+
your program, rather than somewhere on the inside.
|
240
|
+
|
241
|
+
Imagine a program that counts the occurences of a user-defined pattern in a
|
242
|
+
given text file:
|
243
|
+
|
244
|
+
```ruby
|
245
|
+
require 'affect'
|
246
|
+
|
247
|
+
def pattern_count(pattern)
|
248
|
+
total_count = 0
|
249
|
+
found_count = 0
|
250
|
+
while (line = Affect.gets)
|
251
|
+
total_count += 1
|
252
|
+
found_count += 1 if line =~ pattern
|
253
|
+
end
|
254
|
+
Affect.log "found #{found_count} occurrences in #{total_count} lines"
|
255
|
+
found_count
|
256
|
+
end
|
257
|
+
|
258
|
+
Affect.on(
|
259
|
+
gets: -> { Kernel.gets },
|
260
|
+
log: -> { |msg| STDERR << "#{Time.now} #{msg}" }
|
261
|
+
).() {
|
262
|
+
pattern = /#{ARGV[0]}/
|
263
|
+
count = pattern_count(pattern)
|
264
|
+
puts count
|
265
|
+
}
|
266
|
+
```
|
267
|
+
|
268
|
+
In the above example, the `pattern_count` method, which does the "hard work",
|
269
|
+
communicates with the outside world through Affect in order to:
|
270
|
+
|
271
|
+
- read a line after line from some input stream
|
272
|
+
- log an informational message
|
273
|
+
|
274
|
+
Note that `pattern_count` does *not* deal directly with I/O. It does so
|
275
|
+
exclusively through Affect. Testing the method would be much simpler:
|
276
|
+
|
277
|
+
```ruby
|
278
|
+
require 'minitest'
|
279
|
+
require 'affect'
|
280
|
+
|
281
|
+
class PatternCountTest < Minitest::Test
|
282
|
+
def test_correct_count
|
283
|
+
text = StringIO.new("foo\nbar")
|
284
|
+
|
285
|
+
Affect.on(:gets) { text.gets }.on(:log) { |msg| } # ignore
|
286
|
+
.() {
|
287
|
+
count = pattern_count(/foo/)
|
288
|
+
assert_equal(1, count)
|
289
|
+
}
|
290
|
+
end
|
291
|
+
end
|
292
|
+
```
|
293
|
+
|
294
|
+
## Contributing
|
295
|
+
|
296
|
+
Affect is a very small library designed to do very little. If you find it
|
297
|
+
compelling, have encountered any problems using it, or have any suggestions for
|
298
|
+
improvements, please feel free to contribute issues or pull requests.
|
299
|
+
|
300
|
+
##
|
data/affect.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require_relative './lib/affect/version'
|
3
|
+
|
4
|
+
Gem::Specification.new do |s|
|
5
|
+
s.name = 'affect'
|
6
|
+
s.version = Affect::VERSION
|
7
|
+
s.licenses = ['MIT']
|
8
|
+
s.summary = 'Affect: Algebraic Effects for Ruby'
|
9
|
+
s.author = 'Sharon Rosner'
|
10
|
+
s.email = 'ciconia@gmail.com'
|
11
|
+
s.files = `git ls-files`.split
|
12
|
+
s.homepage = 'http://github.com/digital-fabric/affect'
|
13
|
+
s.metadata = {
|
14
|
+
"source_code_uri" => "https://github.com/digital-fabric/affect"
|
15
|
+
}
|
16
|
+
s.rdoc_options = ["--title", "affect", "--main", "README.md"]
|
17
|
+
s.extra_rdoc_files = ["README.md"]
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
# s.add_runtime_dependency 'modulation', '~>0.25'
|
21
|
+
|
22
|
+
s.add_development_dependency 'minitest', '5.11.3'
|
23
|
+
end
|
data/examples/fact.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'affect'
|
3
|
+
|
4
|
+
def fact(x)
|
5
|
+
Affect :log, "calculating factorial for #{x}"
|
6
|
+
(x <= 1) ? 1 : x * fact(x - 1)
|
7
|
+
end
|
8
|
+
|
9
|
+
def main
|
10
|
+
Affect :prompt
|
11
|
+
x = Affect :input
|
12
|
+
result = fact(x)
|
13
|
+
Affect :output, "The factorial of result is #{result}"
|
14
|
+
end
|
15
|
+
|
16
|
+
ctx = Affect.on(
|
17
|
+
prompt: -> { puts "Enter a number: " },
|
18
|
+
input: -> { gets.chomp.to_i },
|
19
|
+
output: ->(msg) { puts msg },
|
20
|
+
log: ->(msg) { puts "#{Time.now} #{msg}" }
|
21
|
+
)
|
22
|
+
|
23
|
+
ctx.() { loop { main } }
|
data/examples/greet.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'affect'
|
3
|
+
|
4
|
+
def main
|
5
|
+
Affect :prompt
|
6
|
+
name = Affect :input
|
7
|
+
Affect :output, "Hi, #{name}! I'm Affected Ruby!"
|
8
|
+
end
|
9
|
+
|
10
|
+
ctx = Affect.on(
|
11
|
+
prompt: -> { puts "Enter your name: " },
|
12
|
+
input: -> { gets.chomp },
|
13
|
+
output: ->(msg) { puts msg }
|
14
|
+
)
|
15
|
+
|
16
|
+
ctx.() { main }
|
data/examples/logging.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'affect'
|
3
|
+
|
4
|
+
def mul(x, y)
|
5
|
+
# assume LOG is a global logger object
|
6
|
+
Affect :log, "called with #{x}, #{y}"
|
7
|
+
x * y
|
8
|
+
end
|
9
|
+
|
10
|
+
Affect.run {
|
11
|
+
puts "Result: #{ mul(2, 3) }"
|
12
|
+
}.on(:log) { |message|
|
13
|
+
puts "#{Time.now} #{message} (this is a log message)"
|
14
|
+
}.()
|
data/examples/pat.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'affect'
|
3
|
+
|
4
|
+
def pattern_count(pattern)
|
5
|
+
total_count = 0
|
6
|
+
found_count = 0
|
7
|
+
while (line = Affect.gets)
|
8
|
+
total_count += 1
|
9
|
+
found_count += 1 if line =~ pattern
|
10
|
+
end
|
11
|
+
Affect.log "found #{found_count} occurrences in #{total_count} lines"
|
12
|
+
found_count
|
13
|
+
end
|
14
|
+
|
15
|
+
Affect.on(
|
16
|
+
gets: -> { STDIN.gets },
|
17
|
+
log: ->(msg) { STDERR.puts "#{Time.now} #{msg}" }
|
18
|
+
).() {
|
19
|
+
pattern = /#{ARGV[0]}/
|
20
|
+
count = pattern_count(pattern)
|
21
|
+
puts count
|
22
|
+
}
|
data/lib/affect.rb
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Affect module
|
4
|
+
module Affect
|
5
|
+
Abort = Object.new # Used as an abort intent
|
6
|
+
|
7
|
+
# Effect context
|
8
|
+
class Context
|
9
|
+
def initialize(&block)
|
10
|
+
@closure = block
|
11
|
+
@handlers = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def on(effect, &block)
|
15
|
+
if effect.is_a?(Hash)
|
16
|
+
@handlers.merge!(effect)
|
17
|
+
else
|
18
|
+
@handlers[effect] = block
|
19
|
+
end
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
def handle(&block)
|
24
|
+
@handlers[nil] = block
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def perform(effect, *args)
|
29
|
+
if (handler = find_handler(effect))
|
30
|
+
call_handler(handler, effect, *args)
|
31
|
+
elsif @parent_context
|
32
|
+
@parent_context.perform(effect, *args)
|
33
|
+
else
|
34
|
+
raise "No effect handler for #{effect.inspect}"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def find_handler(effect)
|
39
|
+
@handlers[effect] || @handlers[effect.class] || @handlers[nil]
|
40
|
+
end
|
41
|
+
|
42
|
+
def call_handler(handler, effect, *args)
|
43
|
+
if handler.arity == 0
|
44
|
+
handler.call
|
45
|
+
elsif args.empty?
|
46
|
+
handler.call(effect)
|
47
|
+
else
|
48
|
+
handler.call(*args)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def abort!(value = nil)
|
53
|
+
throw Abort, (value || Abort)
|
54
|
+
end
|
55
|
+
|
56
|
+
def call(&block)
|
57
|
+
current_thread = Thread.current
|
58
|
+
@parent_context = current_thread[:__affect_context__]
|
59
|
+
current_thread[:__affect_context__] = self
|
60
|
+
catch(Abort) do
|
61
|
+
(block || @closure).call
|
62
|
+
end
|
63
|
+
ensure
|
64
|
+
current_thread[:__affect_context__] = @parent_context
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
class << self
|
69
|
+
def wrap(&block)
|
70
|
+
Context.new(&block)
|
71
|
+
end
|
72
|
+
|
73
|
+
def call(&block)
|
74
|
+
Context.new(&block).call
|
75
|
+
end
|
76
|
+
|
77
|
+
def on(effect, &block)
|
78
|
+
Context.new.on(effect, &block)
|
79
|
+
end
|
80
|
+
|
81
|
+
def handle(&block)
|
82
|
+
Context.new.handle(&block)
|
83
|
+
end
|
84
|
+
|
85
|
+
def current_context
|
86
|
+
Thread.current[:__affect_context__] || (raise 'No effect context present')
|
87
|
+
end
|
88
|
+
|
89
|
+
def perform(effect, *args)
|
90
|
+
current_context.perform(effect, *args)
|
91
|
+
end
|
92
|
+
|
93
|
+
alias_method :method_missing, :perform
|
94
|
+
|
95
|
+
def abort!(value = nil)
|
96
|
+
current_context.abort!(value)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# Kernel extension
|
102
|
+
module Kernel
|
103
|
+
def Affect(effect, *args)
|
104
|
+
Affect.current_context.perform(effect, *args)
|
105
|
+
end
|
106
|
+
end
|
data/test/test_affect.rb
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'minitest/autorun'
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'affect'
|
6
|
+
|
7
|
+
class AffectAPITest < Minitest::Test
|
8
|
+
def test_that_perform_raises_on_no_handler
|
9
|
+
assert_raises RuntimeError do
|
10
|
+
Affect.wrap {
|
11
|
+
Affect.perform :foo
|
12
|
+
}.on(:bar) {
|
13
|
+
:baz
|
14
|
+
}.()
|
15
|
+
end
|
16
|
+
|
17
|
+
# using method call on Affect
|
18
|
+
assert_raises RuntimeError do
|
19
|
+
Affect.wrap {
|
20
|
+
Affect.foo
|
21
|
+
}.()
|
22
|
+
end
|
23
|
+
|
24
|
+
# no raise
|
25
|
+
Affect.wrap {
|
26
|
+
Affect.perform :foo
|
27
|
+
}.on(:foo) {
|
28
|
+
:bar
|
29
|
+
}.()
|
30
|
+
end
|
31
|
+
|
32
|
+
def test_that_emitted_effect_is_performed
|
33
|
+
counter = 0
|
34
|
+
Affect.wrap {
|
35
|
+
3.times { Affect.perform :incr }
|
36
|
+
}.on(:incr) {
|
37
|
+
counter += 1
|
38
|
+
}.()
|
39
|
+
|
40
|
+
assert_equal(3, counter)
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_that_api_methods_return_context
|
44
|
+
o = Affect.wrap {
|
45
|
+
Affect.perform :foo
|
46
|
+
}
|
47
|
+
assert_kind_of(Affect::Context, o)
|
48
|
+
|
49
|
+
o = Affect.on(:foo) {
|
50
|
+
:bar
|
51
|
+
}
|
52
|
+
assert_kind_of(Affect::Context, o)
|
53
|
+
|
54
|
+
o = Affect.handle { }
|
55
|
+
assert_kind_of(Affect::Context, o)
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_that_contexts_can_be_nested
|
59
|
+
results = []
|
60
|
+
o = Affect.wrap {
|
61
|
+
Affect.perform :foo
|
62
|
+
Affect.perform :bar
|
63
|
+
|
64
|
+
Affect.wrap {
|
65
|
+
Affect.perform :foo
|
66
|
+
Affect.perform :bar
|
67
|
+
}
|
68
|
+
.on(:bar) { results << :baz }
|
69
|
+
.()
|
70
|
+
}
|
71
|
+
.on(:foo) { results << :foo }
|
72
|
+
.on(:bar) { results << :bar }
|
73
|
+
.()
|
74
|
+
|
75
|
+
assert_equal([:foo, :bar, :foo, :baz], results)
|
76
|
+
end
|
77
|
+
|
78
|
+
def test_that_effects_can_be_emitted_as_method_calls_on_Affect
|
79
|
+
results = []
|
80
|
+
o = Affect.wrap {
|
81
|
+
Affect.foo
|
82
|
+
Affect.bar
|
83
|
+
|
84
|
+
Affect.wrap {
|
85
|
+
Affect.foo
|
86
|
+
Affect.bar
|
87
|
+
}
|
88
|
+
.on(:bar) { results << :baz }
|
89
|
+
.()
|
90
|
+
}
|
91
|
+
.on(:foo) { results << :foo }
|
92
|
+
.on(:bar) { results << :bar }
|
93
|
+
.()
|
94
|
+
|
95
|
+
assert_equal([:foo, :bar, :foo, :baz], results)
|
96
|
+
end
|
97
|
+
|
98
|
+
def test_that_abort_can_be_called_from_wrapped_code
|
99
|
+
effects = []
|
100
|
+
Affect.handle { |o| effects << o }.() do
|
101
|
+
Affect 1
|
102
|
+
Affect.abort!
|
103
|
+
Affect 2
|
104
|
+
end
|
105
|
+
|
106
|
+
assert_equal([1], effects)
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_that_abort_causes_call_to_return_optional_value
|
110
|
+
o = Affect.() { Affect.abort! }
|
111
|
+
assert_equal(Affect::Abort, o)
|
112
|
+
|
113
|
+
o = Affect.() { Affect.abort!(42) }
|
114
|
+
assert_equal(42, o)
|
115
|
+
end
|
116
|
+
|
117
|
+
class I1; end
|
118
|
+
|
119
|
+
class I2; end
|
120
|
+
|
121
|
+
def test_that_intent_instances_are_handled_correctly
|
122
|
+
results = []
|
123
|
+
Affect
|
124
|
+
.on(I1) { results << :i1 }
|
125
|
+
.on(I2) { results << :i2 }
|
126
|
+
.() {
|
127
|
+
Affect I1.new
|
128
|
+
Affect I2.new
|
129
|
+
}
|
130
|
+
|
131
|
+
assert_equal([:i1, :i2], results)
|
132
|
+
end
|
133
|
+
end
|
metadata
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: affect
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: '0.1'
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sharon Rosner
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-07-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: minitest
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 5.11.3
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 5.11.3
|
27
|
+
description:
|
28
|
+
email: ciconia@gmail.com
|
29
|
+
executables: []
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files:
|
32
|
+
- README.md
|
33
|
+
files:
|
34
|
+
- ".gitignore"
|
35
|
+
- Gemfile
|
36
|
+
- Gemfile.lock
|
37
|
+
- LICENSE
|
38
|
+
- README.md
|
39
|
+
- affect.gemspec
|
40
|
+
- examples/fact.rb
|
41
|
+
- examples/greet.rb
|
42
|
+
- examples/logging.rb
|
43
|
+
- examples/pat.rb
|
44
|
+
- lib/affect.rb
|
45
|
+
- lib/affect/version.rb
|
46
|
+
- test/test_affect.rb
|
47
|
+
homepage: http://github.com/digital-fabric/affect
|
48
|
+
licenses:
|
49
|
+
- MIT
|
50
|
+
metadata:
|
51
|
+
source_code_uri: https://github.com/digital-fabric/affect
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options:
|
54
|
+
- "--title"
|
55
|
+
- affect
|
56
|
+
- "--main"
|
57
|
+
- README.md
|
58
|
+
require_paths:
|
59
|
+
- lib
|
60
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '0'
|
65
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
requirements: []
|
71
|
+
rubygems_version: 3.0.3
|
72
|
+
signing_key:
|
73
|
+
specification_version: 4
|
74
|
+
summary: 'Affect: Algebraic Effects for Ruby'
|
75
|
+
test_files: []
|