affect 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ef9c069aa2f0077d7cbf108e9f9a36aaf2a1808fdc1636d37e40333b6cfc2e0
4
- data.tar.gz: 54e9f068ed3fecdb8dd624efb41b9540ecb1259b569636fca193a7c0fc27e172
3
+ metadata.gz: a0f0602d116f929cf06c3e842419c2bf6b74ccfe87e7af297ccb5cd9a56ea33e
4
+ data.tar.gz: 2b04f09256756633e268cf29d263b870b5065cf5e2bf6a3f55cdac1465a14a0b
5
5
  SHA512:
6
- metadata.gz: d2aadb70ccf3ea71c0dfa794cbe33424a4b2ab74ec6208d0137e3664149320a748199dc6c4944dd677ed3f4ace979e6411878165918e4bbd68c539eadbaa02cc
7
- data.tar.gz: 983d269425f777d7aae8ce64251b293a170a8dc5dfd7a57a46bc0827e7e81c0bfaadd3111d9e578a15676d0621526278054a17c06e6b7c7bd098f2f8d26dfc66
6
+ metadata.gz: aecfbd76f53bf973d597b8ce6233a5a704ef22a2e02b20f88e9b941653d8be4866d5237d9e16c562409e74f7cc86643dfd91384a8707ca82b16e7dd8dd0a5807
7
+ data.tar.gz: 84683a1c5b7f63ee0ab5bfcdb8cd757faef817fd4c0b5da21b93ff10741ca549773068d4ffc7ac34ae95c43a8a9df23f9af0a6908e88c87548399a3699d098ae
@@ -0,0 +1,11 @@
1
+ 0.2 2019-08-26
2
+ --------------
3
+
4
+ * Add alternative effects implementation using fibers
5
+ * Add implementation of delimited continuations
6
+ * Rewrite, change API to use capture/perform
7
+
8
+ 0.1 2019-07-30
9
+ --------------
10
+
11
+ * First implementation
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- affect (0.1)
4
+ affect (0.2)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Affect - structured side effects for functional Ruby
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
- ```bash
24
- $ gem install affect
27
+ ```ruby
28
+ # In your Gemfile
29
+ gem 'affect'
25
30
  ```
26
31
 
27
- Or add it to your Gemfile, you know the drill.
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.wrap {
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 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.
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 `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
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
- ## Performing side effects
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
- Side effects are performed by calling `Affect.perform` or simply `Affect()` with
85
- a specification of the effect to be performed:
93
+ Effect contexts are defined using either `Affect()` or the shorthand
94
+ `Affect.capture`:
86
95
 
87
96
  ```ruby
88
- Affect.perform :foo
97
+ ctx = Affect(log: -> msg { log_msg(msg) })
98
+ ctx.capture { do_something }
89
99
 
90
- # or:
91
- Affect :foo
100
+ # or
101
+ Affect.capture(log: -> msg { log_msg(msg) }) { do_something }
92
102
  ```
93
103
 
94
- You can also pass along more arguments. Those will in turn be passed to the
95
- effect handler:
104
+ The `Affect.capture` method can be called in different manners:
96
105
 
97
106
  ```ruby
98
- Affect :log, 'my message'
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
- 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:
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
- LogIntent = Struct.new(:msg)
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 LogIntent.new('my message')
132
+ Affect.perform :foo # raises an error, as no handler is given for :foo
133
+ }
109
134
  ```
110
135
 
111
- When representing effects using symbols, Affect provides a shorthand way to
112
- perform effects by calling methods directly on the `Affect` module:
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.log('my message')
149
+ Affect(log: -> msg { ... }, ask: -> { ... })
116
150
  ```
117
151
 
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
- `.() { ... }`:
152
+ A catch-all handler can be defined by calling `Affect()` with a block:
121
153
 
122
154
  ```ruby
123
- def add(x, y)
124
- Affect :log, "adding #{x} and #{y}..."
125
- x + y
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
- ## handling side effects
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 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:
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
- # 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
- )
174
+ Affect.perform :foo
175
+
176
+ # or:
177
+ Affect.foo
148
178
  ```
149
179
 
150
- `Affect.handle` is used as a catch-all handler:
180
+ Any parameters will be passed along to the effect handler:
151
181
 
152
182
  ```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
183
+ Affect.perform :log, 'my message'
159
184
  ```
160
185
 
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:
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
- # 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
- ```
190
+ LogIntent = Struct.new(:msg)
187
191
 
188
- ## Putting it all together
192
+ Affect.perform LogIntent.new('my message')
193
+ ```
189
194
 
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:
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 usages
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.on(
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.on(:gets) { text.gets }.on(:log) { |msg| } # ignore
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
@@ -2,36 +2,28 @@
2
2
 
3
3
  # Affect module
4
4
  module Affect
5
- Abort = Object.new # Used as an abort intent
5
+ extend self
6
6
 
7
- # Effect context
7
+ # Implements an effects context
8
8
  class Context
9
- def initialize(&block)
10
- @closure = block
11
- @handlers = {}
9
+ def initialize(handlers = nil, &block)
10
+ @handlers = handlers || { nil => block || -> {} }
12
11
  end
13
12
 
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
13
+ attr_reader :handlers
22
14
 
23
- def handle(&block)
24
- @handlers[nil] = block
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
- if (handler = find_handler(effect))
20
+ handler = find_handler(effect)
21
+ if handler
30
22
  call_handler(handler, effect, *args)
31
- elsif @parent_context
32
- @parent_context.perform(effect, *args)
23
+ elsif @parent
24
+ @parent.perform(effect, *args)
33
25
  else
34
- raise "No effect handler for #{effect.inspect}"
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 == 0
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
- def abort!(value = nil)
53
- throw Abort, (value || Abort)
44
+ @@current = nil
45
+ def self.current
46
+ @@current
54
47
  end
55
48
 
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
49
+ def capture
50
+ @parent, @@current = @@current, self
51
+ catch(:escape) { yield }
63
52
  ensure
64
- current_thread[:__affect_context__] = @parent_context
53
+ @@current = @parent
65
54
  end
66
- end
67
55
 
68
- class << self
69
- def wrap(&block)
70
- Context.new(&block)
56
+ def escape(value = nil)
57
+ throw :escape, (block_given? ? yield : value)
71
58
  end
59
+ end
72
60
 
73
- def call(&block)
74
- Context.new(&block).call
75
- end
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
- def on(effect, &block)
78
- Context.new.on(effect, &block)
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
- def handle(&block)
82
- Context.new.handle(&block)
75
+ def perform(effect, *args)
76
+ unless (ctx = Context.current)
77
+ raise 'perform called outside capture block'
83
78
  end
84
79
 
85
- def current_context
86
- Thread.current[:__affect_context__] || (raise 'No effect context present')
87
- end
80
+ ctx.perform(effect, *args)
81
+ end
88
82
 
89
- def perform(effect, *args)
90
- current_context.perform(effect, *args)
83
+ def escape(value = nil, &block)
84
+ unless (ctx = Context.current)
85
+ raise 'escape called outside capture block'
91
86
  end
92
87
 
93
- alias_method :method_missing, :perform
88
+ ctx.escape(value, &block)
89
+ end
94
90
 
95
- def abort!(value = nil)
96
- current_context.abort!(value)
97
- end
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 extension
100
+ # Kernel extensions
102
101
  module Kernel
103
- def Affect(effect, *args)
104
- Affect.current_context.perform(effect, *args)
102
+ def Affect(handlers = nil, &block)
103
+ Affect::Context.new(handlers, &block)
105
104
  end
106
105
  end
@@ -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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Affect
4
- VERSION = '0.1'
4
+ VERSION = '0.2'
5
5
  end
@@ -4,130 +4,138 @@ require 'minitest/autorun'
4
4
  require 'bundler/setup'
5
5
  require 'affect'
6
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
7
+ class AffectTest < Minitest::Test
8
+ include Affect
16
9
 
17
- # using method call on Affect
18
- assert_raises RuntimeError do
19
- Affect.wrap {
20
- Affect.foo
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 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
- }.()
18
+ def test_escape
19
+ assert_raises(RuntimeError) { escape(:foo) }
20
+ assert_raises(RuntimeError) { escape { :foo } }
39
21
 
40
- assert_equal(3, counter)
22
+ assert_equal :bar, capture { [:foo, escape(:bar)] }
23
+ assert_equal :baz, capture { [:foo, escape { :baz }] }
41
24
  end
42
25
 
43
- def test_that_api_methods_return_context
44
- o = Affect.wrap {
45
- Affect.perform :foo
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
- o = Affect.on(:foo) {
50
- :bar
51
- }
52
- assert_kind_of(Affect::Context, o)
46
+ assert_equal [1, 2, 2, 2, 3, 3], final
47
+ end
53
48
 
54
- o = Affect.handle { }
55
- assert_kind_of(Affect::Context, o)
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
- 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
- .()
63
+ ctx2 = Affect(bar: -> { results << :baz })
64
+ ctx1 = Affect(
65
+ foo: -> { results << :foo },
66
+ bar: -> { results << :bar }
67
+ )
74
68
 
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 {
69
+ ctx1.capture {
81
70
  Affect.foo
82
71
  Affect.bar
83
72
 
84
- Affect.wrap {
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 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)
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
- .on(I1) { results << :i1 }
125
- .on(I2) { results << :i2 }
126
- .() {
127
- Affect I1.new
128
- Affect I2.new
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.1'
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-07-30 00:00:00.000000000 Z
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