affect 0.1 → 0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/Gemfile.lock +1 -1
- data/README.md +103 -101
- data/TODO.md +0 -0
- data/lib/affect.rb +53 -54
- data/lib/affect/cont.rb +49 -0
- data/lib/affect/fiber.rb +114 -0
- data/lib/affect/version.rb +1 -1
- data/test/test_affect.rb +96 -88
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a0f0602d116f929cf06c3e842419c2bf6b74ccfe87e7af297ccb5cd9a56ea33e
|
4
|
+
data.tar.gz: 2b04f09256756633e268cf29d263b870b5065cf5e2bf6a3f55cdac1465a14a0b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: aecfbd76f53bf973d597b8ce6233a5a704ef22a2e02b20f88e9b941653d8be4866d5237d9e16c562409e74f7cc86643dfd91384a8707ca82b16e7dd8dd0a5807
|
7
|
+
data.tar.gz: 84683a1c5b7f63ee0ab5bfcdb8cd757faef817fd4c0b5da21b93ff10741ca549773068d4ffc7ac34ae95c43a8a9df23f9af0a6908e88c87548399a3699d098ae
|
data/CHANGELOG.md
ADDED
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Affect -
|
1
|
+
# Affect - algebraic effects for Ruby
|
2
2
|
|
3
3
|
[INSTALL](#installing-affect) |
|
4
4
|
[TUTORIAL](#getting-started) |
|
@@ -13,6 +13,10 @@ functional programs. Affect implements algebraic effects in Ruby, but can also
|
|
13
13
|
be used to implement patterns that are orthogonal to object-oriented
|
14
14
|
programming, such as inversion of control and dependency injection.
|
15
15
|
|
16
|
+
In addition, Affect includes an alternative implementation of algebraic effects
|
17
|
+
using Ruby fibers, as well as an implementation of delimited continuations using
|
18
|
+
`callcc` (currently deprecated).
|
19
|
+
|
16
20
|
> **Note**: Affect does not pretend to be a *complete, theoretically correct*
|
17
21
|
> implementation of algebraic effects. Affect concentrates on the idea of
|
18
22
|
> [effect contexts](#the-effect-context). It does not deal with continuations,
|
@@ -20,11 +24,12 @@ programming, such as inversion of control and dependency injection.
|
|
20
24
|
|
21
25
|
## Installing Affect
|
22
26
|
|
23
|
-
```
|
24
|
-
|
27
|
+
```ruby
|
28
|
+
# In your Gemfile
|
29
|
+
gem 'affect'
|
25
30
|
```
|
26
31
|
|
27
|
-
Or
|
32
|
+
Or install it manually, you know the drill.
|
28
33
|
|
29
34
|
## Getting Started
|
30
35
|
|
@@ -57,149 +62,146 @@ require 'affect'
|
|
57
62
|
|
58
63
|
def mul(x, y)
|
59
64
|
# assume LOG is a global logger object
|
60
|
-
Affect :log, "called with #{x}, #{y}"
|
65
|
+
Affect.perform :log, "called with #{x}, #{y}"
|
61
66
|
x * y
|
62
67
|
end
|
63
68
|
|
64
|
-
Affect.
|
69
|
+
Affect.capture(
|
70
|
+
log: { |message| puts "#{Time.now} #{message} (this is a log message)" }
|
71
|
+
) {
|
65
72
|
puts "Result: #{ mul(2, 3) }"
|
66
|
-
}.on(:log) { |message|
|
67
|
-
puts "#{Time.now} #{message} (this is a log message)"
|
68
|
-
}.()
|
69
73
|
```
|
70
74
|
|
71
|
-
In the example above, we replace the call to `LOG.info` with
|
72
|
-
|
73
|
-
handler is called in order to perform the
|
75
|
+
In the example above, we replace the call to `LOG.info` with the performance of
|
76
|
+
an *intent* to log a message. When the intent is passed to `Affect`, the
|
77
|
+
corresponding handler is called in order to perform the effect.
|
74
78
|
|
75
79
|
In essence, by separating the performance of side effects into effect intents,
|
76
80
|
and effect handlers, we have separated the what from the how. The `mul` method
|
77
81
|
is no longer concerned with how to log the message it needs to log. There's no
|
78
|
-
hardbaked reference to a `
|
79
|
-
the *intent* to log a message is passed on to
|
82
|
+
hardbaked reference to a `LOG` object, and no logging API to follow. Instead,
|
83
|
+
the *intent* to log a message is passed on to Affect, which in turn runs the
|
80
84
|
correct handler that actually does the logging.
|
81
85
|
|
82
|
-
##
|
86
|
+
## The effect context
|
87
|
+
|
88
|
+
In Affect, effects are performed and handled using an *effect context*. The
|
89
|
+
effect context has one or more effect handlers, and is then used to run code
|
90
|
+
that performs effects, handling effect intents by routing them to the correct
|
91
|
+
handler.
|
83
92
|
|
84
|
-
|
85
|
-
|
93
|
+
Effect contexts are defined using either `Affect()` or the shorthand
|
94
|
+
`Affect.capture`:
|
86
95
|
|
87
96
|
```ruby
|
88
|
-
Affect
|
97
|
+
ctx = Affect(log: -> msg { log_msg(msg) })
|
98
|
+
ctx.capture { do_something }
|
89
99
|
|
90
|
-
# or
|
91
|
-
Affect
|
100
|
+
# or
|
101
|
+
Affect.capture(log: -> msg { log_msg(msg) }) { do_something }
|
92
102
|
```
|
93
103
|
|
94
|
-
|
95
|
-
effect handler:
|
104
|
+
The `Affect.capture` method can be called in different manners:
|
96
105
|
|
97
106
|
```ruby
|
98
|
-
Affect
|
107
|
+
Affect.capture(handler_hash) { body }
|
108
|
+
Affect.capture(handler_proc) { body }
|
109
|
+
Affect.capture(body, handler_hash)
|
110
|
+
Affect.capture(body, handler_proc)
|
99
111
|
```
|
100
112
|
|
101
|
-
|
102
|
-
|
103
|
-
|
113
|
+
... where `body` is the code to be executed, `handler_hash` is a hash of effect
|
114
|
+
handling procs, and `handler_proc` is a default effect handling proc.
|
115
|
+
|
116
|
+
### Nested effect contexts
|
117
|
+
|
118
|
+
Effect contexts can be nested. When an effect context does not know how to
|
119
|
+
handle a certain effect intent, it passes it on to the parent effect context.
|
120
|
+
If no handler has been found for the effect intent, an error is raised:
|
104
121
|
|
105
122
|
```ruby
|
106
|
-
|
123
|
+
# First effect context
|
124
|
+
Affect.capture(log: ->(msg) { LOG.info(msg) }) {
|
125
|
+
Affect.perform :log, 'starting'
|
126
|
+
# Second effect context
|
127
|
+
Affect.capture(log: ->(msg) { }) {
|
128
|
+
Affect.perform :log, 'this message will not be logged'
|
129
|
+
}
|
130
|
+
Affect.perform :log, 'stopping'
|
107
131
|
|
108
|
-
Affect
|
132
|
+
Affect.perform :foo # raises an error, as no handler is given for :foo
|
133
|
+
}
|
109
134
|
```
|
110
135
|
|
111
|
-
|
112
|
-
|
136
|
+
|
137
|
+
## Effect handlers
|
138
|
+
|
139
|
+
Effect handlers map different effects to a proc or a callable object. When an
|
140
|
+
effect is performed, Affect will try to find the relevant effect handler by
|
141
|
+
looking at its *signature* (given as the first argument), and then matching
|
142
|
+
first by value, then by class. Thus, the effect signature can be either a value,
|
143
|
+
or a class (normally used when creating intent classes).
|
144
|
+
|
145
|
+
The simplest, most idiomatic way to define effect handlers is to use symbols as
|
146
|
+
effect signatures:
|
113
147
|
|
114
148
|
```ruby
|
115
|
-
Affect
|
149
|
+
Affect(log: -> msg { ... }, ask: -> { ... })
|
116
150
|
```
|
117
151
|
|
118
|
-
|
119
|
-
`Affect::Context#call`. Mostly, you'll want to use the callable shorthand
|
120
|
-
`.() { ... }`:
|
152
|
+
A catch-all handler can be defined by calling `Affect()` with a block:
|
121
153
|
|
122
154
|
```ruby
|
123
|
-
|
124
|
-
|
125
|
-
|
155
|
+
Affect do |eff, *args|
|
156
|
+
case eff
|
157
|
+
when :log
|
158
|
+
...
|
159
|
+
when :ask
|
160
|
+
...
|
161
|
+
end
|
126
162
|
end
|
127
|
-
|
128
|
-
Affect.on(:log) { |msg| puts "#{Time.now} #{msg}" }.() {
|
129
|
-
result = add(2, 2)
|
130
|
-
puts "result: #{result}"
|
131
|
-
}
|
132
163
|
```
|
133
164
|
|
134
|
-
|
165
|
+
Note that when using a catch-all handler, no error will be raised for unhandled
|
166
|
+
effects.
|
167
|
+
|
168
|
+
## Performing side effects
|
135
169
|
|
136
|
-
Side
|
137
|
-
|
170
|
+
Side effects are performed by calling `Affect.perform` or simply
|
171
|
+
`Affect.<intent>` along with one or more parameters:
|
138
172
|
|
139
173
|
```ruby
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
Affect.on(
|
145
|
-
log: ->(msg) { puts "#{Time.now} #{msg}" },
|
146
|
-
ask: -> { gets.chomp }
|
147
|
-
)
|
174
|
+
Affect.perform :foo
|
175
|
+
|
176
|
+
# or:
|
177
|
+
Affect.foo
|
148
178
|
```
|
149
179
|
|
150
|
-
|
180
|
+
Any parameters will be passed along to the effect handler:
|
151
181
|
|
152
182
|
```ruby
|
153
|
-
Affect.
|
154
|
-
case effect
|
155
|
-
when :log then puts "#{Time.now} #{msg}"
|
156
|
-
when :ask then gets.chomp
|
157
|
-
end
|
158
|
-
end
|
183
|
+
Affect.perform :log, 'my message'
|
159
184
|
```
|
160
185
|
|
161
|
-
|
162
|
-
|
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:
|
186
|
+
Effects intents can be represented using any Ruby object, but in a relatively
|
187
|
+
complex application might best be represented using classes or structs:
|
172
188
|
|
173
189
|
```ruby
|
174
|
-
|
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
|
-
```
|
190
|
+
LogIntent = Struct.new(:msg)
|
187
191
|
|
188
|
-
|
192
|
+
Affect.perform LogIntent.new('my message')
|
193
|
+
```
|
189
194
|
|
190
|
-
|
191
|
-
|
195
|
+
When using symbols as effect signatures, Affect provides a shorthand way to
|
196
|
+
perform effects by calling methods directly on the `Affect` module:
|
192
197
|
|
193
198
|
```ruby
|
194
|
-
Affect
|
195
|
-
.on(:ask) { ... }
|
196
|
-
.on(:tell) { ... }
|
197
|
-
.() do
|
198
|
-
...
|
199
|
-
end
|
199
|
+
Affect.log('my message')
|
200
200
|
```
|
201
201
|
|
202
|
-
## Other
|
202
|
+
## Other uses
|
203
|
+
|
204
|
+
In addition to isolating side-effects, Affect can be used for other purposes:
|
203
205
|
|
204
206
|
### Dependency injection
|
205
207
|
|
@@ -255,10 +257,10 @@ def pattern_count(pattern)
|
|
255
257
|
found_count
|
256
258
|
end
|
257
259
|
|
258
|
-
Affect
|
260
|
+
Affect(
|
259
261
|
gets: -> { Kernel.gets },
|
260
262
|
log: -> { |msg| STDERR << "#{Time.now} #{msg}" }
|
261
|
-
).
|
263
|
+
).capture {
|
262
264
|
pattern = /#{ARGV[0]}/
|
263
265
|
count = pattern_count(pattern)
|
264
266
|
puts count
|
@@ -282,8 +284,10 @@ class PatternCountTest < Minitest::Test
|
|
282
284
|
def test_correct_count
|
283
285
|
text = StringIO.new("foo\nbar")
|
284
286
|
|
285
|
-
Affect
|
286
|
-
|
287
|
+
Affect(
|
288
|
+
gets: -> { text.gets },
|
289
|
+
log: -> |msg| {} # ignore
|
290
|
+
.capture {
|
287
291
|
count = pattern_count(/foo/)
|
288
292
|
assert_equal(1, count)
|
289
293
|
}
|
@@ -295,6 +299,4 @@ end
|
|
295
299
|
|
296
300
|
Affect is a very small library designed to do very little. If you find it
|
297
301
|
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
|
-
##
|
302
|
+
improvements, please feel free to contribute issues or pull requests.
|
data/TODO.md
ADDED
File without changes
|
data/lib/affect.rb
CHANGED
@@ -2,36 +2,28 @@
|
|
2
2
|
|
3
3
|
# Affect module
|
4
4
|
module Affect
|
5
|
-
|
5
|
+
extend self
|
6
6
|
|
7
|
-
#
|
7
|
+
# Implements an effects context
|
8
8
|
class Context
|
9
|
-
def initialize(&block)
|
10
|
-
@
|
11
|
-
@handlers = {}
|
9
|
+
def initialize(handlers = nil, &block)
|
10
|
+
@handlers = handlers || { nil => block || -> {} }
|
12
11
|
end
|
13
12
|
|
14
|
-
|
15
|
-
if effect.is_a?(Hash)
|
16
|
-
@handlers.merge!(effect)
|
17
|
-
else
|
18
|
-
@handlers[effect] = block
|
19
|
-
end
|
20
|
-
self
|
21
|
-
end
|
13
|
+
attr_reader :handlers
|
22
14
|
|
23
|
-
def
|
24
|
-
|
25
|
-
self
|
15
|
+
def handler_proc
|
16
|
+
proc { |effect, *args| handle(effect, *args) }
|
26
17
|
end
|
27
18
|
|
28
19
|
def perform(effect, *args)
|
29
|
-
|
20
|
+
handler = find_handler(effect)
|
21
|
+
if handler
|
30
22
|
call_handler(handler, effect, *args)
|
31
|
-
elsif @
|
32
|
-
@
|
23
|
+
elsif @parent
|
24
|
+
@parent.perform(effect, *args)
|
33
25
|
else
|
34
|
-
raise "No
|
26
|
+
raise "No handler found for #{effect.inspect}"
|
35
27
|
end
|
36
28
|
end
|
37
29
|
|
@@ -40,7 +32,7 @@ module Affect
|
|
40
32
|
end
|
41
33
|
|
42
34
|
def call_handler(handler, effect, *args)
|
43
|
-
if handler.arity
|
35
|
+
if handler.arity.zero?
|
44
36
|
handler.call
|
45
37
|
elsif args.empty?
|
46
38
|
handler.call(effect)
|
@@ -49,58 +41,65 @@ module Affect
|
|
49
41
|
end
|
50
42
|
end
|
51
43
|
|
52
|
-
|
53
|
-
|
44
|
+
@@current = nil
|
45
|
+
def self.current
|
46
|
+
@@current
|
54
47
|
end
|
55
48
|
|
56
|
-
def
|
57
|
-
|
58
|
-
|
59
|
-
current_thread[:__affect_context__] = self
|
60
|
-
catch(Abort) do
|
61
|
-
(block || @closure).call
|
62
|
-
end
|
49
|
+
def capture
|
50
|
+
@parent, @@current = @@current, self
|
51
|
+
catch(:escape) { yield }
|
63
52
|
ensure
|
64
|
-
|
53
|
+
@@current = @parent
|
65
54
|
end
|
66
|
-
end
|
67
55
|
|
68
|
-
|
69
|
-
|
70
|
-
Context.new(&block)
|
56
|
+
def escape(value = nil)
|
57
|
+
throw :escape, (block_given? ? yield : value)
|
71
58
|
end
|
59
|
+
end
|
72
60
|
|
73
|
-
|
74
|
-
|
75
|
-
|
61
|
+
def capture(*args, &block)
|
62
|
+
block, handlers = block_and_handlers_from_args(*args, &block)
|
63
|
+
handlers = { nil => handlers } if handlers.is_a?(Proc)
|
64
|
+
Context.new(handlers).capture(&block)
|
65
|
+
end
|
76
66
|
|
77
|
-
|
78
|
-
|
67
|
+
def block_and_handlers_from_args(*args, &block)
|
68
|
+
case args.size
|
69
|
+
when 1 then block ? [block, args.first] : [args.first, nil]
|
70
|
+
when 2 then args
|
71
|
+
else [block, nil]
|
79
72
|
end
|
73
|
+
end
|
80
74
|
|
81
|
-
|
82
|
-
|
75
|
+
def perform(effect, *args)
|
76
|
+
unless (ctx = Context.current)
|
77
|
+
raise 'perform called outside capture block'
|
83
78
|
end
|
84
79
|
|
85
|
-
|
86
|
-
|
87
|
-
end
|
80
|
+
ctx.perform(effect, *args)
|
81
|
+
end
|
88
82
|
|
89
|
-
|
90
|
-
|
83
|
+
def escape(value = nil, &block)
|
84
|
+
unless (ctx = Context.current)
|
85
|
+
raise 'escape called outside capture block'
|
91
86
|
end
|
92
87
|
|
93
|
-
|
88
|
+
ctx.escape(value, &block)
|
89
|
+
end
|
94
90
|
|
95
|
-
|
96
|
-
|
97
|
-
|
91
|
+
def respond_to_missing?(*)
|
92
|
+
true
|
93
|
+
end
|
94
|
+
|
95
|
+
def method_missing(*args)
|
96
|
+
perform(*args)
|
98
97
|
end
|
99
98
|
end
|
100
99
|
|
101
|
-
# Kernel
|
100
|
+
# Kernel extensions
|
102
101
|
module Kernel
|
103
|
-
def Affect(
|
104
|
-
Affect.
|
102
|
+
def Affect(handlers = nil, &block)
|
103
|
+
Affect::Context.new(handlers, &block)
|
105
104
|
end
|
106
105
|
end
|
data/lib/affect/cont.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# adapted from:
|
4
|
+
# https://github.com/mveytsman/DelimR/blob/master/lib/delimr.rb
|
5
|
+
|
6
|
+
require 'continuation'
|
7
|
+
|
8
|
+
module Affect
|
9
|
+
module Cont
|
10
|
+
extend self
|
11
|
+
|
12
|
+
# holds objects of the form [bool, Continuation]
|
13
|
+
# where bool siginifies a
|
14
|
+
@@stack = []
|
15
|
+
|
16
|
+
def abort(v)
|
17
|
+
(@@stack.pop)[1].(v)
|
18
|
+
end
|
19
|
+
|
20
|
+
def capture(&block)
|
21
|
+
callcc { |outer|
|
22
|
+
@@stack << [true, outer]
|
23
|
+
abort(block.())
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
def escape
|
28
|
+
callcc do |esc|
|
29
|
+
unwound_continuations = unwind_stack
|
30
|
+
cont_proc = lambda { |v|
|
31
|
+
callcc do |ret|
|
32
|
+
@@stack << [true, ret]
|
33
|
+
unwound_continuations.each { |c| @@stack << [nil, c] }
|
34
|
+
esc.call(v)
|
35
|
+
end
|
36
|
+
}
|
37
|
+
abort(yield(cont_proc))
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def unwind_stack
|
42
|
+
unwound = []
|
43
|
+
while @@stack.last && !(@@stack.last)[0]
|
44
|
+
unwound << (@@stack.pop)[1]
|
45
|
+
end
|
46
|
+
unwound.reverse
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
data/lib/affect/fiber.rb
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'fiber'
|
4
|
+
|
5
|
+
# Affect module
|
6
|
+
module Affect
|
7
|
+
module Fiber
|
8
|
+
extend self
|
9
|
+
|
10
|
+
class Intent
|
11
|
+
def initialize(*args)
|
12
|
+
@args = args
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :args
|
16
|
+
end
|
17
|
+
|
18
|
+
class Escape < Intent
|
19
|
+
def initialize(&block)
|
20
|
+
@block = block
|
21
|
+
end
|
22
|
+
|
23
|
+
def call(*args)
|
24
|
+
@block.(*args)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def capture(*args, &block)
|
29
|
+
block, handler = case args.size
|
30
|
+
when 1 then block ? [block, args.first] : [args.first, nil]
|
31
|
+
when 2 then args
|
32
|
+
else [block, nil]
|
33
|
+
end
|
34
|
+
|
35
|
+
f = Fiber.new(&block)
|
36
|
+
v = f.resume
|
37
|
+
loop do
|
38
|
+
break v unless f.alive? && v.is_a?(Intent)
|
39
|
+
|
40
|
+
if v.is_a?(Escape)
|
41
|
+
break v.()
|
42
|
+
else
|
43
|
+
v = f.resume(handler.(*v.args))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def perform(*args)
|
49
|
+
Fiber.yield Intent.new(*args)
|
50
|
+
rescue FiberError
|
51
|
+
raise RuntimeError, 'perform called outside of capture'
|
52
|
+
end
|
53
|
+
|
54
|
+
def escape(value = nil, &block)
|
55
|
+
block ||= proc { value }
|
56
|
+
Fiber.yield Escape.new(&block)
|
57
|
+
rescue FiberError
|
58
|
+
raise RuntimeError, 'escape called outside of capture'
|
59
|
+
end
|
60
|
+
|
61
|
+
def method_missing(*args)
|
62
|
+
perform(*args)
|
63
|
+
end
|
64
|
+
|
65
|
+
class Context
|
66
|
+
def initialize(handlers = nil, &block)
|
67
|
+
@handlers = handlers || { nil => block || -> { } }
|
68
|
+
end
|
69
|
+
|
70
|
+
attr_reader :handlers
|
71
|
+
|
72
|
+
def handler_proc
|
73
|
+
proc { |effect, *args| handle(effect, *args) }
|
74
|
+
end
|
75
|
+
|
76
|
+
def handle(effect, *args)
|
77
|
+
handler = find_handler(effect)
|
78
|
+
if handler
|
79
|
+
call_handler(handler, effect, *args)
|
80
|
+
else
|
81
|
+
begin
|
82
|
+
Fiber.yield Intent.new(effect, *args)
|
83
|
+
rescue FiberError
|
84
|
+
raise RuntimeError, "No handler found for #{effect.inspect}"
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def find_handler(effect)
|
90
|
+
@handlers[effect] || @handlers[effect.class] || @handlers[nil]
|
91
|
+
end
|
92
|
+
|
93
|
+
def call_handler(handler, effect, *args)
|
94
|
+
if handler.arity == 0
|
95
|
+
handler.call
|
96
|
+
elsif args.empty?
|
97
|
+
handler.call(effect)
|
98
|
+
else
|
99
|
+
handler.call(*args)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def capture(&block)
|
104
|
+
Affect.capture(block, handler_proc)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
module Kernel
|
111
|
+
def Affect(handlers = nil, &block)
|
112
|
+
Affect::Context.new(handlers, &block)
|
113
|
+
end
|
114
|
+
end
|
data/lib/affect/version.rb
CHANGED
data/test/test_affect.rb
CHANGED
@@ -4,130 +4,138 @@ require 'minitest/autorun'
|
|
4
4
|
require 'bundler/setup'
|
5
5
|
require 'affect'
|
6
6
|
|
7
|
-
class
|
8
|
-
|
9
|
-
assert_raises RuntimeError do
|
10
|
-
Affect.wrap {
|
11
|
-
Affect.perform :foo
|
12
|
-
}.on(:bar) {
|
13
|
-
:baz
|
14
|
-
}.()
|
15
|
-
end
|
7
|
+
class AffectTest < Minitest::Test
|
8
|
+
include Affect
|
16
9
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
end
|
10
|
+
def test_capture_with_different_arities
|
11
|
+
assert_equal :foo, capture { :foo }
|
12
|
+
assert_equal :foo, capture(-> { :foo })
|
13
|
+
|
14
|
+
assert_equal :bar, capture(-> { perform(:foo) }, ->(e) { e == :foo && :bar })
|
23
15
|
|
24
|
-
# no raise
|
25
|
-
Affect.wrap {
|
26
|
-
Affect.perform :foo
|
27
|
-
}.on(:foo) {
|
28
|
-
:bar
|
29
|
-
}.()
|
30
16
|
end
|
31
17
|
|
32
|
-
def
|
33
|
-
|
34
|
-
|
35
|
-
3.times { Affect.perform :incr }
|
36
|
-
}.on(:incr) {
|
37
|
-
counter += 1
|
38
|
-
}.()
|
18
|
+
def test_escape
|
19
|
+
assert_raises(RuntimeError) { escape(:foo) }
|
20
|
+
assert_raises(RuntimeError) { escape { :foo } }
|
39
21
|
|
40
|
-
assert_equal
|
22
|
+
assert_equal :bar, capture { [:foo, escape(:bar)] }
|
23
|
+
assert_equal :baz, capture { [:foo, escape { :baz }] }
|
41
24
|
end
|
42
25
|
|
43
|
-
def
|
44
|
-
|
45
|
-
|
26
|
+
def test_effect_handler_dsl
|
27
|
+
v = 1
|
28
|
+
ctx = Affect(
|
29
|
+
get: -> { v },
|
30
|
+
set: ->(x) { v = x }
|
31
|
+
)
|
32
|
+
|
33
|
+
assert_kind_of Affect::Context, ctx
|
34
|
+
|
35
|
+
final = ctx.capture {
|
36
|
+
[
|
37
|
+
perform(:get),
|
38
|
+
perform(:set, 2),
|
39
|
+
perform(:get),
|
40
|
+
Affect.get,
|
41
|
+
Affect.set(3),
|
42
|
+
Affect.get
|
43
|
+
]
|
46
44
|
}
|
47
|
-
assert_kind_of(Affect::Context, o)
|
48
45
|
|
49
|
-
|
50
|
-
|
51
|
-
}
|
52
|
-
assert_kind_of(Affect::Context, o)
|
46
|
+
assert_equal [1, 2, 2, 2, 3, 3], final
|
47
|
+
end
|
53
48
|
|
54
|
-
|
55
|
-
|
49
|
+
def test_context_missing_handler
|
50
|
+
assert_raises RuntimeError do
|
51
|
+
Affect(foo: -> { :bar }).capture { perform :baz }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def test_context_wildcard_handler
|
56
|
+
ctx = Affect do |e| e + 1; end
|
57
|
+
assert_equal 3, ctx.capture { perform(2) }
|
56
58
|
end
|
57
59
|
|
58
60
|
def test_that_contexts_can_be_nested
|
59
61
|
results = []
|
60
|
-
o = Affect.wrap {
|
61
|
-
Affect.perform :foo
|
62
|
-
Affect.perform :bar
|
63
62
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
}
|
68
|
-
|
69
|
-
.()
|
70
|
-
}
|
71
|
-
.on(:foo) { results << :foo }
|
72
|
-
.on(:bar) { results << :bar }
|
73
|
-
.()
|
63
|
+
ctx2 = Affect(bar: -> { results << :baz })
|
64
|
+
ctx1 = Affect(
|
65
|
+
foo: -> { results << :foo },
|
66
|
+
bar: -> { results << :bar }
|
67
|
+
)
|
74
68
|
|
75
|
-
|
76
|
-
end
|
77
|
-
|
78
|
-
def test_that_effects_can_be_emitted_as_method_calls_on_Affect
|
79
|
-
results = []
|
80
|
-
o = Affect.wrap {
|
69
|
+
ctx1.capture {
|
81
70
|
Affect.foo
|
82
71
|
Affect.bar
|
83
72
|
|
84
|
-
|
73
|
+
ctx2.capture {
|
85
74
|
Affect.foo
|
86
75
|
Affect.bar
|
87
76
|
}
|
88
|
-
.on(:bar) { results << :baz }
|
89
|
-
.()
|
90
77
|
}
|
91
|
-
.on(:foo) { results << :foo }
|
92
|
-
.on(:bar) { results << :bar }
|
93
|
-
.()
|
94
78
|
|
95
79
|
assert_equal([:foo, :bar, :foo, :baz], results)
|
96
80
|
end
|
97
81
|
|
98
|
-
def
|
99
|
-
|
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)
|
82
|
+
def test_that_escape_provides_return_value_of_capture
|
83
|
+
assert_equal 42, capture { 2 * escape { 42 } }
|
115
84
|
end
|
116
85
|
|
117
86
|
class I1; end
|
118
|
-
|
119
87
|
class I2; end
|
120
88
|
|
121
89
|
def test_that_intent_instances_are_handled_correctly
|
122
90
|
results = []
|
123
|
-
Affect
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
91
|
+
Affect(
|
92
|
+
I1 => -> { results << :i1 },
|
93
|
+
I2 => -> { results << :i2 }
|
94
|
+
).capture {
|
95
|
+
perform I1.new
|
96
|
+
perform I2.new
|
129
97
|
}
|
130
98
|
|
131
99
|
assert_equal([:i1, :i2], results)
|
132
100
|
end
|
101
|
+
|
102
|
+
# doesn't work with callback-based affect
|
103
|
+
def test_that_capture_can_work_across_fibers_with_transfer
|
104
|
+
require 'fiber'
|
105
|
+
f1 = Fiber.new { |f| escape(:foo) }
|
106
|
+
f2 = Fiber.new { f1.transfer(f2) }
|
107
|
+
|
108
|
+
# assert_equal :foo, capture { f2.resume }
|
109
|
+
end
|
110
|
+
|
111
|
+
# doesn't work with fiber-based Affect
|
112
|
+
def test_that_capture_can_work_across_fibers_with_yield
|
113
|
+
f1 = Fiber.new { |f| escape(:foo) }
|
114
|
+
f2 = Fiber.new { f1.resume }
|
115
|
+
|
116
|
+
# assert_equal :foo, capture { f2.resume }
|
117
|
+
end
|
133
118
|
end
|
119
|
+
|
120
|
+
class ContTest < Minitest::Test
|
121
|
+
require 'affect/cont'
|
122
|
+
|
123
|
+
Cont = Affect::Cont
|
124
|
+
|
125
|
+
def test_that_continuation_is_provided_to_escape
|
126
|
+
k = Cont.capture { 2 * Cont.escape { |cont| cont } }
|
127
|
+
assert_kind_of Proc, k
|
128
|
+
end
|
129
|
+
|
130
|
+
def test_that_continuation_completes_the_computation_in_capture
|
131
|
+
k = Cont.capture { 2 * Cont.escape { |cont| cont } }
|
132
|
+
assert_equal 12, k.(6)
|
133
|
+
end
|
134
|
+
|
135
|
+
def test_that_continuation_can_be_called_multiple_times
|
136
|
+
k = Cont.capture { 2 * Cont.escape { |cont| cont } }
|
137
|
+
assert_equal 4, k.(2)
|
138
|
+
assert_equal 6, k.(3)
|
139
|
+
assert_equal 8, k.(4)
|
140
|
+
end
|
141
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: affect
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: '0.
|
4
|
+
version: '0.2'
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-08-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: minitest
|
@@ -32,16 +32,20 @@ extra_rdoc_files:
|
|
32
32
|
- README.md
|
33
33
|
files:
|
34
34
|
- ".gitignore"
|
35
|
+
- CHANGELOG.md
|
35
36
|
- Gemfile
|
36
37
|
- Gemfile.lock
|
37
38
|
- LICENSE
|
38
39
|
- README.md
|
40
|
+
- TODO.md
|
39
41
|
- affect.gemspec
|
40
42
|
- examples/fact.rb
|
41
43
|
- examples/greet.rb
|
42
44
|
- examples/logging.rb
|
43
45
|
- examples/pat.rb
|
44
46
|
- lib/affect.rb
|
47
|
+
- lib/affect/cont.rb
|
48
|
+
- lib/affect/fiber.rb
|
45
49
|
- lib/affect/version.rb
|
46
50
|
- test/test_affect.rb
|
47
51
|
homepage: http://github.com/digital-fabric/affect
|