dispatch 0.0.1pre
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.
- data/.gitignore +4 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +14 -0
- data/README.rdoc +670 -0
- data/Rakefile +2 -0
- data/dispatch.gemspec +21 -0
- data/examples/benchmarks.rb +90 -0
- data/examples/dispatch_methods.rb +277 -0
- data/examples/dispatch_methods.sh +5 -0
- data/examples/futures.rb +34 -0
- data/examples/ring_buffer.rb +95 -0
- data/examples/sleeping_barber.rb +19 -0
- data/lib/dispatch.rb +22 -0
- data/lib/dispatch/enumerable.rb +108 -0
- data/lib/dispatch/job.rb +52 -0
- data/lib/dispatch/proxy.rb +49 -0
- data/lib/dispatch/queue.rb +34 -0
- data/lib/dispatch/source.rb +83 -0
- data/lib/dispatch/version.rb +3 -0
- data/spec/enumerable_spec.rb +188 -0
- data/spec/job_spec.rb +103 -0
- data/spec/proxy_spec.rb +105 -0
- data/spec/queue_spec.rb +45 -0
- data/spec/source_spec.rb +190 -0
- data/spec/spec_helper.rb +60 -0
- metadata +99 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
# A GCD-based implementation of the sleeping barber problem:
|
2
|
+
# http://en.wikipedia.org/wiki/Sleeping_barber_problem
|
3
|
+
# http://www.madebysofa.com/#blog/the_sleeping_barber
|
4
|
+
|
5
|
+
waiting_chairs = Dispatch::Queue.new('com.apple.waiting_chairs')
|
6
|
+
semaphore = Dispatch::Semaphore.new(3)
|
7
|
+
index = -1
|
8
|
+
while true
|
9
|
+
index += 1
|
10
|
+
success = semaphore.wait(Dispatch::TIME_NOW)
|
11
|
+
if success != 0
|
12
|
+
puts "Customer turned away #{index}"
|
13
|
+
next
|
14
|
+
end
|
15
|
+
waiting_chairs.async do
|
16
|
+
semaphore.signal
|
17
|
+
puts "Shave and a haircut #{index}"
|
18
|
+
end
|
19
|
+
end
|
data/lib/dispatch.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
#
|
2
|
+
# dispatch.rb - Grand Central Dispatch support library
|
3
|
+
#
|
4
|
+
# Copyright (C) 2010 Apple, Inc.
|
5
|
+
#
|
6
|
+
# == Overview ==
|
7
|
+
#
|
8
|
+
# Grand Central Dispatch (GCD) is a novel approach to multicore computing
|
9
|
+
# first released in Mac OS X version 10.6 Snow Leopard.
|
10
|
+
# The Dispatch module and associated classes (Queue, Group, Semaphore, Source)
|
11
|
+
# in MacRuby core provides a simple wrapping on top of the libdispatch C API.
|
12
|
+
# This library provides higher-level services and convenience methods
|
13
|
+
# to make it easier for traditional Ruby programmers to add multicore support.
|
14
|
+
|
15
|
+
libdir = File.expand_path(File.dirname(__FILE__))
|
16
|
+
$LOAD_PATH.unshift(libdir) unless $LOAD_PATH.include?(libdir)
|
17
|
+
|
18
|
+
require 'dispatch/source'
|
19
|
+
require 'dispatch/queue'
|
20
|
+
require 'dispatch/proxy'
|
21
|
+
require 'dispatch/job'
|
22
|
+
require 'dispatch/enumerable'
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# Additional parallel operations for any object supporting +each+
|
2
|
+
|
3
|
+
module Dispatch
|
4
|
+
class Queue
|
5
|
+
def fake_apply(n, &block)
|
6
|
+
g = Dispatch::Group.new
|
7
|
+
n.times do |i|
|
8
|
+
async(g) { block.call(i) }
|
9
|
+
end
|
10
|
+
g.wait
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
class Integer
|
17
|
+
# Applies the +&block+ +Integer+ number of times in parallel
|
18
|
+
# -- passing in stride (default 1) iterations at a time --
|
19
|
+
# on a concurrent queue of the given (optional) +priority+
|
20
|
+
#
|
21
|
+
# @sum = 0
|
22
|
+
# 10.p_times(3) { |j| @sum += j }
|
23
|
+
# p @sum # => 55
|
24
|
+
#
|
25
|
+
def p_times(stride=1, priority=nil, &block)
|
26
|
+
n_times = self.to_int
|
27
|
+
q = Dispatch::Queue.concurrent(priority)
|
28
|
+
return q.apply(n_times, &block) if stride == 1
|
29
|
+
|
30
|
+
n_strides = (n_times / stride).to_int
|
31
|
+
block_from = Proc.new do |j0|
|
32
|
+
lambda { |j| block.call(j0+j) }
|
33
|
+
end
|
34
|
+
q.fake_apply(n_strides) { |i| stride.times &block_from.call(i*stride) }
|
35
|
+
# Runs the remainder (if any) sequentially on the current thread
|
36
|
+
(n_times % stride).times &block_from.call(n_strides*stride)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
module Enumerable
|
41
|
+
|
42
|
+
# Parallel +each+
|
43
|
+
def p_each(stride=1, priority=nil, &block)
|
44
|
+
ary = self.to_a
|
45
|
+
ary.count.p_times(stride, priority) { |i| block.call(ary[i]) }
|
46
|
+
end
|
47
|
+
|
48
|
+
# Parallel +each+
|
49
|
+
def p_each_with_index(stride=1, priority=nil, &block)
|
50
|
+
ary = self.to_a
|
51
|
+
ary.count.p_times(stride, priority) { |i| block.call(ary[i], i) }
|
52
|
+
end
|
53
|
+
|
54
|
+
# Parallel +collect+
|
55
|
+
# Results match the order of the original array
|
56
|
+
def p_map(stride=1, priority=nil, &block)
|
57
|
+
@p_map_result = Dispatch::Proxy.new([])
|
58
|
+
@p_map_result_q ||= Dispatch::Queue.for(@p_map_result)
|
59
|
+
@p_map_result_q.sync do
|
60
|
+
self.p_each_with_index(stride, priority) { |obj, i| @p_map_result[i] = block.call(obj) }
|
61
|
+
end
|
62
|
+
@p_map_result.__value__
|
63
|
+
end
|
64
|
+
|
65
|
+
# Parallel +collect+ plus +inject+
|
66
|
+
# Accumulates from +initial+ via +op+ (default = '+')
|
67
|
+
# Note: each object can only run one p_mapreduce at a time
|
68
|
+
def p_mapreduce(initial, op=:+, stride=1, priority=nil, &block)
|
69
|
+
# Check first, since exceptions from a Dispatch block can act funky
|
70
|
+
raise ArgumentError if not initial.respond_to? op
|
71
|
+
@mapreduce_q ||= Dispatch::Queue.for(self)
|
72
|
+
@mapreduce_q.sync do # in case called more than once at a time
|
73
|
+
@mapreduce_result = initial
|
74
|
+
q = Dispatch::Queue.for(@mapreduce_result)
|
75
|
+
self.p_each(stride, priority) do |obj|
|
76
|
+
val = block.call(obj)
|
77
|
+
q.async { @mapreduce_result = @mapreduce_result.send(op, val) }
|
78
|
+
end
|
79
|
+
q.sync {}
|
80
|
+
return @mapreduce_result
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Parallel +select+; will return array of objects for which
|
85
|
+
# +&block+ returns true.
|
86
|
+
def p_find_all(stride=1, priority=nil, &block)
|
87
|
+
found_all = Dispatch::Proxy.new([])
|
88
|
+
self.p_each(stride, priority) { |obj| found_all << obj if block.call(obj) }
|
89
|
+
found_all.__value__
|
90
|
+
end
|
91
|
+
|
92
|
+
# Parallel +detect+; will return -one- match for +&block+
|
93
|
+
# but it may not be the 'first'
|
94
|
+
# Only useful if the test block is very expensive to run
|
95
|
+
# Note: each object can only run one p_find at a time
|
96
|
+
def p_find(stride=1, priority=nil, &block)
|
97
|
+
@find_q ||= Dispatch::Queue.for(self)
|
98
|
+
@find_q.sync do
|
99
|
+
@find_result = nil
|
100
|
+
q = Dispatch::Queue.for(@find_result)
|
101
|
+
self.p_each(stride, priority) do |obj|
|
102
|
+
q.async { @find_result = obj } if @find_result.nil? and block.call(obj)
|
103
|
+
end
|
104
|
+
q.sync {}
|
105
|
+
return @find_result
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
data/lib/dispatch/job.rb
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
module Dispatch
|
2
|
+
|
3
|
+
# Track completion and return values of asynchronous requests
|
4
|
+
# Duck-type +join+ and +value+ from +Thread+
|
5
|
+
class Job
|
6
|
+
# Create a Job that asynchronously dispatches the block
|
7
|
+
attr_reader :group, :values
|
8
|
+
|
9
|
+
def initialize(queue = Dispatch::Queue.concurrent, &block)
|
10
|
+
@queue = queue
|
11
|
+
@group = Group.new
|
12
|
+
@values = synchronize([])
|
13
|
+
add(&block) if not block.nil?
|
14
|
+
end
|
15
|
+
|
16
|
+
def synchronize(obj)
|
17
|
+
Dispatch::Proxy.new(obj, @group)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Submit block as part of the same dispatch group
|
21
|
+
def add(&block)
|
22
|
+
@queue.async(@group) { @values << block.call }
|
23
|
+
end
|
24
|
+
|
25
|
+
# Wait until execution has completed.
|
26
|
+
# If a +block+ is passed, invoke that asynchronously
|
27
|
+
# on the specified +queue+ (or else the default queue).
|
28
|
+
def join(queue = Dispatch::Queue.concurrent, &block)
|
29
|
+
return group.wait if block.nil?
|
30
|
+
group.notify(queue) { block.call }
|
31
|
+
end
|
32
|
+
|
33
|
+
# Wait then return the next value; note: only ordered if a serial queue
|
34
|
+
# If a +block+ is passed, invoke that asynchronously with the value
|
35
|
+
# on the specified +queue+ (or else the default queue).
|
36
|
+
def value(queue = Dispatch::Queue.concurrent, &block)
|
37
|
+
return group.notify(queue) { block.call(result) } if not block.nil?
|
38
|
+
group.wait
|
39
|
+
return result
|
40
|
+
end
|
41
|
+
|
42
|
+
alias_method :sync, :synchronize
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
# Remove and return the first value
|
47
|
+
def result
|
48
|
+
@values[-1]
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require 'delegate'
|
2
|
+
|
3
|
+
module Dispatch
|
4
|
+
# Serialize or asynchronize access to a delegate object.
|
5
|
+
# Forwards method invocations to the passed object via a private serial queue,
|
6
|
+
# and can call back asynchronously if given a block
|
7
|
+
#
|
8
|
+
class Proxy < SimpleDelegator
|
9
|
+
|
10
|
+
attr_accessor :__group__, :__queue__, :__sync__
|
11
|
+
|
12
|
+
# Create Proxy to wrap the given +delegate+,
|
13
|
+
# optionally specify +group+ and +queue+ for asynchronous callbacks
|
14
|
+
def initialize(delegate, group=Group.new, queue=Dispatch::Queue.concurrent)
|
15
|
+
super(delegate)
|
16
|
+
@__serial__ = Dispatch::Queue.for(self)
|
17
|
+
@__group__ = group
|
18
|
+
@__queue__ = queue
|
19
|
+
@__retval__ = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
# Call methods on the +delegate+ object via a private serial queue
|
23
|
+
# Returns asychronously if given a block; else synchronously
|
24
|
+
#
|
25
|
+
def method_missing(symbol, *args, &block)
|
26
|
+
if block.nil? then
|
27
|
+
@__serial__.sync { @__retval__ = __getobj__.__send__(symbol,*args) }
|
28
|
+
return @__retval__
|
29
|
+
end
|
30
|
+
queue = @__queue__ # copy in case it changes while in flight
|
31
|
+
@__serial__.async(@__group__) do
|
32
|
+
retval = __getobj__.__send__(symbol, *args)
|
33
|
+
queue.async(@__group__) { block.call(retval) }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Wait until the internal private queue has completed pending executions
|
38
|
+
def __wait__
|
39
|
+
@__serial__.sync { }
|
40
|
+
end
|
41
|
+
|
42
|
+
# Return the +delegate+ object after waiting
|
43
|
+
def __value__
|
44
|
+
__wait__
|
45
|
+
__getobj__
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# Adds convenience methods to Queues
|
2
|
+
|
3
|
+
module Dispatch
|
4
|
+
class Queue
|
5
|
+
|
6
|
+
# Returns a mostly unique reverse-DNS-style label based on
|
7
|
+
# the ancestor chain and ID of +obj+ plus the current time
|
8
|
+
#
|
9
|
+
# Dispatch::Queue.labelize(Array.new)
|
10
|
+
# => enumerable.array.0x2000cc2c0.1265915278.97557
|
11
|
+
#
|
12
|
+
def self.labelize(obj)
|
13
|
+
names = obj.class.ancestors[0...-2].map {|a| a.to_s.downcase}
|
14
|
+
label = names.uniq.reverse.join(".")
|
15
|
+
"#{label}.0x%x.#{Time.now.to_f}" % obj.object_id
|
16
|
+
end
|
17
|
+
|
18
|
+
# Returns a new serial queue with a unique label based on +obj+
|
19
|
+
# Typically used to serialize access to that object
|
20
|
+
#
|
21
|
+
# a = Array.new
|
22
|
+
# q = Dispatch::Queue.for(a)
|
23
|
+
# q.async { a << 2 }
|
24
|
+
#
|
25
|
+
def self.for(obj)
|
26
|
+
new(labelize(obj))
|
27
|
+
end
|
28
|
+
|
29
|
+
# Wait until pending blocks have completed
|
30
|
+
def join
|
31
|
+
sync {}
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# Adds convenience methods to Queues for creating Sources
|
2
|
+
|
3
|
+
module Dispatch
|
4
|
+
class Source
|
5
|
+
|
6
|
+
@@events = {
|
7
|
+
exit:PROC_EXIT,
|
8
|
+
fork:PROC_FORK,
|
9
|
+
exec:PROC_EXEC,
|
10
|
+
signal:PROC_SIGNAL,
|
11
|
+
|
12
|
+
delete:VNODE_DELETE,
|
13
|
+
write:VNODE_WRITE,
|
14
|
+
extend:VNODE_EXTEND,
|
15
|
+
attrib:VNODE_ATTRIB,
|
16
|
+
link:VNODE_LINK,
|
17
|
+
rename:VNODE_RENAME,
|
18
|
+
revoke:VNODE_REVOKE
|
19
|
+
}
|
20
|
+
|
21
|
+
class << self
|
22
|
+
|
23
|
+
def event2num(e)
|
24
|
+
return 0 if e.nil?
|
25
|
+
value = e.to_int rescue @@events[e.to_sym]
|
26
|
+
raise ArgumentError, "No event type #{e.inspect}" if value.nil?
|
27
|
+
value
|
28
|
+
end
|
29
|
+
|
30
|
+
def events2mask(events)
|
31
|
+
mask = events.collect { |e| event2num(e) }.reduce(:|)
|
32
|
+
end
|
33
|
+
|
34
|
+
def data2events(bitmask)
|
35
|
+
@@events.collect { |k,v| k if (v & bitmask) > 0 }.compact
|
36
|
+
end
|
37
|
+
|
38
|
+
# Returns Dispatch::Source of type DATA_ADD
|
39
|
+
def add(queue = Dispatch::Queue.concurrent, &block)
|
40
|
+
Dispatch::Source.new(Dispatch::Source::DATA_ADD, 0, 0, queue, &block)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns Dispatch::Source of type DATA_OR
|
44
|
+
def or(queue = Dispatch::Queue.concurrent, &block)
|
45
|
+
Dispatch::Source.new(Dispatch::Source::DATA_OR, 0, 0, queue, &block)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Takes events: :delete, :write, :extend, :attrib, :link, :rename, :revoke
|
49
|
+
# Returns Dispatch::Source of type PROC
|
50
|
+
def process(pid, events, queue = Dispatch::Queue.concurrent, &block)
|
51
|
+
events = events2mask(events) if not events.respond_to? :to_int
|
52
|
+
Dispatch::Source.new(Dispatch::Source::PROC, pid, events, queue, &block)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Returns Dispatch::Source of type SIGNAL
|
56
|
+
def signal(signal, queue = Dispatch::Queue.concurrent, &block)
|
57
|
+
signal = Signal.list[signal.to_s] if signal.to_i == 0
|
58
|
+
Dispatch::Source.new(Dispatch::Source::SIGNAL, signal, 0, queue, &block)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns Dispatch::Source of type READ
|
62
|
+
def read(file, queue = Dispatch::Queue.concurrent, &block)
|
63
|
+
Dispatch::Source.new(Dispatch::Source::READ, file, 0, queue, &block)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Returns Dispatch::Source of type WRITE
|
67
|
+
def write(file, queue = Dispatch::Queue.concurrent, &block)
|
68
|
+
Dispatch::Source.new(Dispatch::Source::WRITE, file, 0, queue, &block)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Takes events: :exit, :fork, :exec, :signal
|
72
|
+
# Returns Dispatch::Source of type VNODE
|
73
|
+
def file(file, events, queue = Dispatch::Queue.concurrent, &block)
|
74
|
+
events = events2mask(events) if not events.respond_to? :to_int
|
75
|
+
Dispatch::Source.new(Dispatch::Source::VNODE, file, events, queue, &block)
|
76
|
+
end
|
77
|
+
|
78
|
+
def periodic(seconds, queue = Dispatch::Queue.concurrent, &block)
|
79
|
+
Dispatch::Source.timer(0, seconds, 0, queue, &block)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
if MACOSX_VERSION >= 10.6
|
4
|
+
describe "parallel loop" do
|
5
|
+
|
6
|
+
describe :Integer do
|
7
|
+
describe :p_times do
|
8
|
+
before :each do
|
9
|
+
@count = 4
|
10
|
+
@ary = Array.new
|
11
|
+
@p_ary = Dispatch::Proxy.new []
|
12
|
+
end
|
13
|
+
|
14
|
+
it "runs the block that many times" do
|
15
|
+
@count.times { |j| @ary << 1 }
|
16
|
+
@count.p_times { |j| @p_ary << 1 }
|
17
|
+
@p_ary.size.should == @ary.size
|
18
|
+
end
|
19
|
+
|
20
|
+
it "runs the block passing the current index" do
|
21
|
+
@count.times { |j| @ary << j }
|
22
|
+
@count.p_times { |j| @p_ary << j}
|
23
|
+
@p_ary.sort.should == @ary
|
24
|
+
end
|
25
|
+
|
26
|
+
it "does not run the block if the count is zero" do
|
27
|
+
0.p_times { |j| @ary << 1 }
|
28
|
+
@ary.size.should == 0
|
29
|
+
end
|
30
|
+
|
31
|
+
it "properly combines blocks with even stride > 1" do
|
32
|
+
@count.times { |j| @ary << j }
|
33
|
+
@count.p_times(2) { |j| @p_ary << j}
|
34
|
+
@p_ary.sort.should == @ary
|
35
|
+
end
|
36
|
+
|
37
|
+
it "properly combines blocks with uneven stride" do
|
38
|
+
@count.times { |j| @ary << j }
|
39
|
+
@count.p_times(3) { |j| @p_ary << j}
|
40
|
+
@p_ary.sort.should == @ary
|
41
|
+
end
|
42
|
+
|
43
|
+
it "properly rounds stride fraction of 0.5" do
|
44
|
+
6.times { |j| @ary << j }
|
45
|
+
6.p_times(4) { |j| @p_ary << j}
|
46
|
+
@p_ary.sort.should == @ary
|
47
|
+
end
|
48
|
+
|
49
|
+
it "properly rounds stride fraction > 0.5" do
|
50
|
+
7.times { |j| @ary << j }
|
51
|
+
7.p_times(4) { |j| @p_ary << j}
|
52
|
+
@p_ary.sort.should == @ary
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "Enumerable" do
|
58
|
+
before :each do
|
59
|
+
@rng = (1..3)
|
60
|
+
@ary = @rng.to_a
|
61
|
+
end
|
62
|
+
|
63
|
+
describe :p_each do
|
64
|
+
it "exists on objects that support Enumerable" do
|
65
|
+
@ary.respond_to?(:p_each).should == true
|
66
|
+
@rng.respond_to?(:p_each).should == true
|
67
|
+
end
|
68
|
+
|
69
|
+
it "should behave like each" do
|
70
|
+
@ary1 = 0
|
71
|
+
@ary.each {|v| @ary1 << v*v}
|
72
|
+
@ary2 = 0
|
73
|
+
@q = Dispatch::Queue.for(@ary2)
|
74
|
+
@ary.p_each {|v| temp = v*v; @q.sync {@ary2 << temp} }
|
75
|
+
@ary2.should == @ary1
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should work with ranges" do
|
79
|
+
@ary1 = 0
|
80
|
+
@ary.each {|v| @ary1 << v*v}
|
81
|
+
@ary2 = 0
|
82
|
+
@q = Dispatch::Queue.for(@ary2)
|
83
|
+
@rng.p_each {|v| temp = v*v; @q.sync {@ary2 << temp} }
|
84
|
+
@ary2.should == @ary1
|
85
|
+
end
|
86
|
+
|
87
|
+
it "should execute concurrently" do
|
88
|
+
t0 = Time.now
|
89
|
+
@ary.p_each {|v| sleep v/100.0}
|
90
|
+
t1 = Time.now
|
91
|
+
t_total = @ary.inject(0) {|a,b| a + b/100.0}
|
92
|
+
(t1-t0).to_f.should < t_total
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe :p_each_with_index do
|
97
|
+
it "exists on objects that support Enumerable" do
|
98
|
+
@ary.respond_to?(:p_each).should == true
|
99
|
+
end
|
100
|
+
|
101
|
+
it "should behave like each_with_index" do
|
102
|
+
@ary1 = 0
|
103
|
+
@ary.each_with_index {|v, i| @ary1 << v**i}
|
104
|
+
@ary2 = 0
|
105
|
+
@q = Dispatch::Queue.for(@ary2)
|
106
|
+
@ary.p_each_with_index {|v, i| temp = v**i; @q.sync {@ary2 << temp} }
|
107
|
+
@ary2.should == @ary1
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
describe :p_map do
|
112
|
+
it "exists on objects that support Enumerable" do
|
113
|
+
@ary.respond_to?(:p_map).should == true
|
114
|
+
end
|
115
|
+
|
116
|
+
it "should behave like map" do
|
117
|
+
map1 = @ary.map {|v| v*v}
|
118
|
+
map2 = @ary.p_map {|v| v*v}
|
119
|
+
map2.should == map1
|
120
|
+
end
|
121
|
+
|
122
|
+
it "should stride safely" do
|
123
|
+
map1 = @ary.map {|v| v*v}
|
124
|
+
map2 = @ary.p_map(2) {|v| v*v}
|
125
|
+
map2.should == map1
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
describe :p_mapreduce do
|
130
|
+
it "exists on objects that support Enumerable" do
|
131
|
+
@ary.respond_to?(:p_mapreduce).should == true
|
132
|
+
end
|
133
|
+
|
134
|
+
it "should behave like an unordered map" do
|
135
|
+
map1 = @ary.map {|v| v*v}
|
136
|
+
map2 = @ary.p_mapreduce([]) {|v| [v*v]}
|
137
|
+
map2.sort.should == map1
|
138
|
+
end
|
139
|
+
|
140
|
+
it "should accumulate any object that takes :+ " do
|
141
|
+
map1 = @ary.map {|v| "%x" % (10+v)}
|
142
|
+
map2 = @ary.p_mapreduce("") {|v| "%x" % (10+v)}
|
143
|
+
map1.each do |s|
|
144
|
+
map2.index(s).should_not == nil
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
it "should allow custom accumulator methods" do
|
149
|
+
map1 = @ary.map {|v| v**2}
|
150
|
+
sum1 = map1.inject(0) {|s,v| s | v}
|
151
|
+
sum2 = @ary.p_mapreduce(0, :|) {|v| v**2}
|
152
|
+
sum2.should == sum1
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
describe :p_find_all do
|
157
|
+
it "exists on objects that support Enumerable" do
|
158
|
+
@ary.respond_to?(:p_find_all).should == true
|
159
|
+
end
|
160
|
+
|
161
|
+
it "should behave like find_all" do
|
162
|
+
found1 = @ary.find_all {|v| v.odd?}
|
163
|
+
found2 = @ary.p_find_all {|v| v.odd?}
|
164
|
+
found2.sort.should == found1
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
describe :p_find do
|
169
|
+
it "exists on objects that support Enumerable" do
|
170
|
+
@ary.respond_to?(:p_find).should == true
|
171
|
+
end
|
172
|
+
|
173
|
+
it "returns nil if nothing found" do
|
174
|
+
found2 = @ary.p_find {|v| false}
|
175
|
+
found2.should.nil?
|
176
|
+
end
|
177
|
+
|
178
|
+
it "returns one element that matches the condition" do
|
179
|
+
found1 = @ary.find_all {|v| v.odd?}
|
180
|
+
found2 = @ary.p_find {|v| v.odd?}
|
181
|
+
found2.should_not.nil?
|
182
|
+
found1.include? found2
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
end
|
188
|
+
end
|