Skip to content

Commit 43c4a2d

Browse files
committed
Merge pull request #158 from obrok/promise-composition
Promise composition
2 parents 7cd201e + 3cb66e0 commit 43c4a2d

File tree

2 files changed

+100
-0
lines changed

2 files changed

+100
-0
lines changed

lib/concurrent/promise.rb

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,51 @@ def rescue(&block)
110110
alias_method :catch, :rescue
111111
alias_method :on_error, :rescue
112112

113+
# Yield the successful result to the block that returns a promise. If that
114+
# promise is also successful the result is the result of the yielded promise.
115+
# If either part fails the whole also fails.
116+
#
117+
# @example
118+
# Promise.execute { 1 }.flat_map { |v| Promise.execute { v + 2 } }.value! #=> 3
119+
#
120+
# @return [Promise]
121+
def flat_map(&block)
122+
child = Promise.new(
123+
parent: self,
124+
executor: ImmediateExecutor.new,
125+
)
126+
127+
on_error { |e| child.on_reject(e) }
128+
on_success do |result1|
129+
begin
130+
inner = block.call(result1)
131+
inner.execute
132+
inner.on_success { |result2| child.on_fulfill(result2) }
133+
inner.on_error { |e| child.on_reject(e) }
134+
rescue => e
135+
child.on_reject(e)
136+
end
137+
end
138+
139+
child
140+
end
141+
142+
# Builds a promise that produces the result of self and others in an Array
143+
# and fails if any of them fails.
144+
#
145+
# @param [Array<Promise>] others
146+
#
147+
# @return [Promise]
148+
def zip(*others)
149+
others.reduce(self.then { |x| [x] }) do |p1, p2|
150+
p1.flat_map do |results|
151+
p2.then do |next_result|
152+
results << next_result
153+
end
154+
end
155+
end
156+
end
157+
113158
protected
114159

115160
def set_pending

spec/concurrent/promise_spec.rb

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,61 @@ module Concurrent
260260
end
261261
end
262262

263+
describe '#flat_map' do
264+
265+
it 'returns a promise' do
266+
child = empty_root.flat_map { nil }
267+
expect(child).to be_a Promise
268+
expect(child).not_to be empty_root
269+
end
270+
271+
it 'succeeds if both promises succeed' do
272+
child = Promise.new(executor: executor) { 1 }.
273+
flat_map { |v| Promise.new(executor: executor) { v + 10 } }.execute.wait
274+
275+
expect(child.value!).to eq(11)
276+
end
277+
278+
it 'fails if the left promise fails' do
279+
child = Promise.new(executor: executor) { fail }.
280+
flat_map { |v| Promise.new(executor: executor) { v + 10 } }.execute.wait
281+
282+
expect(child).to be_rejected
283+
end
284+
285+
it 'fails if the right promise fails' do
286+
child = Promise.new(executor: executor) { 1 }.
287+
flat_map { |v| Promise.new(executor: executor) { fail } }.execute.wait
288+
289+
expect(child).to be_rejected
290+
end
291+
292+
it 'fails if the generating block fails' do
293+
child = Promise.new(executor: executor) { }.flat_map { fail }.execute.wait
294+
295+
expect(child).to be_rejected
296+
end
297+
298+
end
299+
300+
describe '#zip' do
301+
let(:promise1) { Promise.new(executor: executor) { 1 } }
302+
let(:promise2) { Promise.new(executor: executor) { 2 } }
303+
let(:promise3) { Promise.new(executor: executor) { [3] } }
304+
305+
it 'yields the results as an array' do
306+
composite = promise1.zip(promise2, promise3).execute.wait
307+
308+
expect(composite.value).to eq([1, 2, [3]])
309+
end
310+
311+
it 'fails if one component fails' do
312+
composite = promise1.zip(promise2, rejected_subject, promise3).execute.wait
313+
314+
expect(composite).to be_rejected
315+
end
316+
end
317+
263318
context 'fulfillment' do
264319

265320
it 'passes the result of each block to all its children' do

0 commit comments

Comments
 (0)