affect 0.1 → 0.2
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 +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
|