immutable 0.2.0 → 0.3.0
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/README.md +0 -9
- data/benchmark/benchmark_deque.rb +59 -0
- data/benchmark/benchmark_map.rb +5 -5
- data/benchmark/benchmark_parallel_fib.rb +73 -0
- data/benchmark/benchmark_queue.rb +3 -3
- data/lib/immutable.rb +7 -5
- data/lib/immutable/consable.rb +344 -0
- data/lib/immutable/deque.rb +196 -0
- data/lib/immutable/foldable.rb +10 -0
- data/lib/immutable/headable.rb +235 -0
- data/lib/immutable/list.rb +23 -487
- data/lib/immutable/map.rb +23 -22
- data/lib/immutable/output_restricted_deque.rb +20 -0
- data/lib/immutable/queue.rb +31 -38
- data/lib/immutable/stream.rb +75 -186
- data/test/immutable/test_deque.rb +116 -0
- data/test/immutable/test_list.rb +62 -11
- data/test/immutable/test_map.rb +2 -11
- data/test/immutable/test_promise.rb +2 -2
- data/test/immutable/test_queue.rb +31 -4
- data/test/immutable/test_stream.rb +26 -16
- data/test/test_helper.rb +2 -0
- metadata +9 -2
data/README.md
CHANGED
@@ -3,15 +3,6 @@ immutable - immutable data structures for Ruby
|
|
3
3
|
|
4
4
|
This project aims to provide immutable data structures for Ruby.
|
5
5
|
|
6
|
-
Currently, immutable provides the following classes and modules:
|
7
|
-
|
8
|
-
* Immutable::Foldable
|
9
|
-
* Immutable::List
|
10
|
-
* Immutable::Map
|
11
|
-
* Immutable::Promise
|
12
|
-
* Immutable::Stream
|
13
|
-
* Immutable::Queue
|
14
|
-
|
15
6
|
Install
|
16
7
|
========================
|
17
8
|
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require "benchmark"
|
2
|
+
require_relative "../lib/immutable/deque"
|
3
|
+
require_relative "../lib/immutable/map"
|
4
|
+
|
5
|
+
TIMES = 100000
|
6
|
+
key_size = 10
|
7
|
+
|
8
|
+
def head(deque)
|
9
|
+
TIMES.times do
|
10
|
+
deque.head
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def last(deque)
|
15
|
+
TIMES.times do
|
16
|
+
deque.last
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def tail(deque)
|
21
|
+
TIMES.times do
|
22
|
+
deque.tail
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def init(deque)
|
27
|
+
TIMES.times do
|
28
|
+
deque.init
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def cons(deque)
|
33
|
+
TIMES.times do
|
34
|
+
deque.cons(0)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def snoc(deque)
|
39
|
+
TIMES.times do
|
40
|
+
deque.snoc(0)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def run(bm, deque, num, method)
|
45
|
+
bm.report("#{method} for #{num}-elements deque") do
|
46
|
+
send(method, deque)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
Benchmark.bmbm do |bm|
|
51
|
+
deques = [100, 1000, 10000].inject(Immutable::Map.empty) { |m, n|
|
52
|
+
m.insert(n, (1..n).inject(Immutable::Deque.empty, &:snoc))
|
53
|
+
}
|
54
|
+
for method in [:head, :last, :tail, :init, :cons, :snoc]
|
55
|
+
for num, deque in deques
|
56
|
+
run(bm, deque, num, method)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/benchmark/benchmark_map.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
4
|
-
|
5
|
-
require
|
1
|
+
require "benchmark"
|
2
|
+
require "avl_tree"
|
3
|
+
require "red_black_tree"
|
4
|
+
require_relative "../lib/immutable/map"
|
5
|
+
require "openssl"
|
6
6
|
|
7
7
|
#random = Random.new(0)
|
8
8
|
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
require 'avl_tree'
|
3
|
+
require 'red_black_tree'
|
4
|
+
require 'immutable/map'
|
5
|
+
require 'atomic'
|
6
|
+
|
7
|
+
|
8
|
+
class LockBoundFib
|
9
|
+
def initialize(map = RedBlackTree.new)
|
10
|
+
@memo = map
|
11
|
+
@mutex = Mutex.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def [](n)
|
15
|
+
@mutex.synchronize { @memo[n] } ||
|
16
|
+
if n < 2
|
17
|
+
n
|
18
|
+
else
|
19
|
+
self[n - 2] + self[n - 1]
|
20
|
+
end.tap { |x|
|
21
|
+
@mutex.synchronize {
|
22
|
+
@memo[n] = x
|
23
|
+
}
|
24
|
+
}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class LockFreeFib
|
29
|
+
def initialize(map = Immutable::Map.empty)
|
30
|
+
@memo = Atomic.new(map)
|
31
|
+
end
|
32
|
+
|
33
|
+
def [](n)
|
34
|
+
@memo.value[n] ||
|
35
|
+
if n < 2
|
36
|
+
n
|
37
|
+
else
|
38
|
+
self[n - 2] + self[n - 1]
|
39
|
+
end.tap { |x|
|
40
|
+
@memo.update { |map|
|
41
|
+
map.insert(n, x)
|
42
|
+
}
|
43
|
+
}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
TIMES = 5
|
48
|
+
N = 10000
|
49
|
+
|
50
|
+
Benchmark.bmbm do |bm|
|
51
|
+
bm.report("lock-bound") do
|
52
|
+
TIMES.times do
|
53
|
+
lock_bound_fib = LockBoundFib.new(RedBlackTree.new)
|
54
|
+
threads = 4.times.map {
|
55
|
+
Thread.start {
|
56
|
+
lock_bound_fib[N]
|
57
|
+
}
|
58
|
+
}
|
59
|
+
threads.each(&:join)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
bm.report("lock-free") do
|
63
|
+
TIMES.times do
|
64
|
+
lock_free_fib = LockFreeFib.new(Immutable::Map.empty)
|
65
|
+
threads = 4.times.map {
|
66
|
+
Thread.start {
|
67
|
+
lock_free_fib[N]
|
68
|
+
}
|
69
|
+
}
|
70
|
+
threads.each(&:join)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/lib/immutable.rb
CHANGED
@@ -2,8 +2,10 @@
|
|
2
2
|
module Immutable
|
3
3
|
end
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
require_relative "immutable/list"
|
6
|
+
require_relative "immutable/map"
|
7
|
+
require_relative "immutable/promise"
|
8
|
+
require_relative "immutable/stream"
|
9
|
+
require_relative "immutable/queue"
|
10
|
+
require_relative "immutable/output_restricted_deque"
|
11
|
+
require_relative "immutable/deque"
|
@@ -0,0 +1,344 @@
|
|
1
|
+
require_relative "headable"
|
2
|
+
|
3
|
+
module Immutable
|
4
|
+
module Consable
|
5
|
+
include Headable
|
6
|
+
|
7
|
+
module ClassMethods
|
8
|
+
# Creates a new +Consable+ object populated with the given objects.
|
9
|
+
#
|
10
|
+
# @param [Array<Object>] elements the elements of the +Consable+
|
11
|
+
# object.
|
12
|
+
# @return [Consable] the new +Consable+ object.
|
13
|
+
def [](*elements)
|
14
|
+
from_array(elements)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Converts the given array to a +Consable+ object.
|
18
|
+
#
|
19
|
+
# @param [Array, #reverse_each] ary the array to convert.
|
20
|
+
# @return [Consable] the +Consable+ object converted from +ary+.
|
21
|
+
def from_array(ary)
|
22
|
+
ary.reverse_each.inject(empty) { |x, y|
|
23
|
+
Cons(y, x)
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
# Converts the given Enumerable object to a +Consable+ object.
|
28
|
+
#
|
29
|
+
# @param [#inject] enum the Enumerable object to convert.
|
30
|
+
# @return [Cons] the +Consable+ object converted from +enum+.
|
31
|
+
def from_enum(enum)
|
32
|
+
enum.inject(empty) { |x, y|
|
33
|
+
Cons(y, x)
|
34
|
+
}.reverse
|
35
|
+
end
|
36
|
+
|
37
|
+
# Builds a +Consable+ object from the seed value +e+ and the given
|
38
|
+
# block. The block takes a seed value and returns +nil+ if the seed
|
39
|
+
# should unfold to the empty +Consable+ object, or returns +[a, b]+,
|
40
|
+
# where +a+ is the head of the +Consable+ object and +b+ is the next
|
41
|
+
# seed from which to unfold the tail. For example:
|
42
|
+
#
|
43
|
+
# xs = List.unfoldr(3) { |x| x == 0 ? nil : [x, x - 1] }
|
44
|
+
# p xs #=> List[3, 2, 1]
|
45
|
+
#
|
46
|
+
# +unfoldr+ is the dual of +foldr+.
|
47
|
+
#
|
48
|
+
# @param [Object] e the seed value.
|
49
|
+
# @return [Consable] the +Consable+ object built from the seed value
|
50
|
+
# and the block.
|
51
|
+
def unfoldr(e, &block)
|
52
|
+
x = yield(e)
|
53
|
+
if x.nil?
|
54
|
+
empty
|
55
|
+
else
|
56
|
+
y, z = x
|
57
|
+
Cons(y, unfoldr(z, &block))
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def Cons(x, y)
|
64
|
+
y.cons(x)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.included(c)
|
69
|
+
c.extend(ClassMethods)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Adds a new element at the head of +self+. A class including
|
73
|
+
# +Immutable::Consable+ must implement this method.
|
74
|
+
#
|
75
|
+
# @param [Object] x the element to add.
|
76
|
+
# @return [Consable] a new +Consable+ object.
|
77
|
+
def cons(x)
|
78
|
+
raise NotImplementedError
|
79
|
+
end
|
80
|
+
|
81
|
+
# Same as {#cons}. This method just calls {#cons}.
|
82
|
+
#
|
83
|
+
# @param [Object] x the element to add.
|
84
|
+
# @return [Consable] a new +Consable+ object.
|
85
|
+
def unshift(x)
|
86
|
+
cons(x)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Same as {#cons}. This method just calls {#cons}.
|
90
|
+
#
|
91
|
+
# @param [Object] x the element to add.
|
92
|
+
# @return [Consable] a new +Consable+ object.
|
93
|
+
def prepend(x)
|
94
|
+
cons(x)
|
95
|
+
end
|
96
|
+
|
97
|
+
# Appends two +Consable+ objects +self+ and +xs+.
|
98
|
+
#
|
99
|
+
# @param [Consable] xs the +Consable+ object to append.
|
100
|
+
# @return [Consable] a new +Consable+ object.
|
101
|
+
def +(xs)
|
102
|
+
foldr(xs) { |y, ys| Cons(y, ys) }
|
103
|
+
end
|
104
|
+
|
105
|
+
# Returns the +Consable+ object obtained by applying the given block to
|
106
|
+
# each element in +self+.
|
107
|
+
#
|
108
|
+
# @return [Consable] the obtained +Consable+ object.
|
109
|
+
def map(&block)
|
110
|
+
rev_map(&block).reverse
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns the +Consable+ object obtained by applying the given block to
|
114
|
+
# each element in +self+ in reverse order.
|
115
|
+
#
|
116
|
+
# @return [Consable] the obtained +Consable+ object.
|
117
|
+
def rev_map
|
118
|
+
foldl(empty) { |xs, x| Cons(yield(x), xs) }
|
119
|
+
end
|
120
|
+
|
121
|
+
# Returns the elements of +self+ in reverse order.
|
122
|
+
#
|
123
|
+
# @return [Consable] the reversed +Consable+ object.
|
124
|
+
def reverse
|
125
|
+
foldl(empty) { |x, y| Cons(y, x) }
|
126
|
+
end
|
127
|
+
|
128
|
+
# Returns a new +Consable+ object obtained by inserting +sep+ in between
|
129
|
+
# the elements of +self+.
|
130
|
+
#
|
131
|
+
# @param [Object] sep the object to insert between elements.
|
132
|
+
# @return [Consable] the new +Consable+ object.
|
133
|
+
def intersperse(sep)
|
134
|
+
if empty?
|
135
|
+
empty
|
136
|
+
else
|
137
|
+
Cons(head, tail.prepend_to_all(sep))
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Returns a new +Consable+ object obtained by inserting +xs+ in between
|
142
|
+
# the +Consable+ objects in +self+ and concatenates the result.
|
143
|
+
# +xss.intercalate(xs)+ is equivalent to +xss.intersperse(xs).flatten+.
|
144
|
+
#
|
145
|
+
# @param [Consable] xs the +Consable+ object to insert between
|
146
|
+
# +Consable+ objects.
|
147
|
+
# @return [Consable] the new +Consable+ object.
|
148
|
+
def intercalate(xs)
|
149
|
+
intersperse(xs).flatten
|
150
|
+
end
|
151
|
+
|
152
|
+
# Transposes the rows and columns of +self+. For example:
|
153
|
+
#
|
154
|
+
# p List[List[1, 2, 3], List[4, 5, 6]].transpose
|
155
|
+
# #=> List[List[1, 4], List[2, 5], List[3, 6]]
|
156
|
+
#
|
157
|
+
# @return [Consable] the transposed +Consable+ object.
|
158
|
+
def transpose
|
159
|
+
if empty?
|
160
|
+
empty
|
161
|
+
else
|
162
|
+
if head.empty?
|
163
|
+
tail.transpose
|
164
|
+
else
|
165
|
+
t = tail.filter { |x| !x.empty? }
|
166
|
+
Cons(Cons(head.head, t.map(&:head)),
|
167
|
+
Cons(head.tail, t.map(&:tail)).transpose)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
# Returns the +Consable+ object of all subsequences of +self+.
|
173
|
+
#
|
174
|
+
# @return [Consable<Consable>] the subsequences of +self+.
|
175
|
+
def subsequences
|
176
|
+
Cons(empty, nonempty_subsequences)
|
177
|
+
end
|
178
|
+
|
179
|
+
# Concatenates a +Consable+ object of +Consable+ objects.
|
180
|
+
#
|
181
|
+
# @return [Consable] the concatenated +Consable+ object.
|
182
|
+
def flatten
|
183
|
+
foldr(empty) { |x, xs| x + xs }
|
184
|
+
end
|
185
|
+
|
186
|
+
alias concat flatten
|
187
|
+
|
188
|
+
# Returns the +Consable+ object obtained by concatenating the results of
|
189
|
+
# the given block for each element in +self+.
|
190
|
+
#
|
191
|
+
# @return [Consable] the obtained +Consable+ object.
|
192
|
+
def flat_map
|
193
|
+
foldr(empty) { |x, xs| yield(x) + xs }
|
194
|
+
end
|
195
|
+
|
196
|
+
alias concat_map flat_map
|
197
|
+
alias bind flat_map
|
198
|
+
|
199
|
+
# Returns the first +n+ elements of +self+, or all the elements of
|
200
|
+
# +self+ if +n > self.length+.
|
201
|
+
#
|
202
|
+
# @param [Integer] n the number of elements to take.
|
203
|
+
# @return [Consable] the first +n+ elements of +self+.
|
204
|
+
def take(n)
|
205
|
+
if empty?
|
206
|
+
empty
|
207
|
+
else
|
208
|
+
if n <= 0
|
209
|
+
empty
|
210
|
+
else
|
211
|
+
Cons(head, tail.take(n - 1))
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
# Returns the suffix of +self+ after the first +n+ elements, or
|
217
|
+
# an empty +Consable+ object if +n > self.length+.
|
218
|
+
#
|
219
|
+
# @param [Integer] n the number of elements to drop.
|
220
|
+
# @return [Consable] the suffix of +self+ after the first +n+ elements.
|
221
|
+
def drop(n)
|
222
|
+
if empty?
|
223
|
+
empty
|
224
|
+
else
|
225
|
+
if n > 0
|
226
|
+
tail.drop(n - 1)
|
227
|
+
else
|
228
|
+
self
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
# Returns the longest prefix of the elements of +self+ for which +block+
|
234
|
+
# evaluates to true.
|
235
|
+
#
|
236
|
+
# @return [Consable] the prefix of the elements of +self+.
|
237
|
+
def take_while(&block)
|
238
|
+
if empty?
|
239
|
+
empty
|
240
|
+
else
|
241
|
+
if yield(head)
|
242
|
+
Cons(head, tail.take_while(&block))
|
243
|
+
else
|
244
|
+
empty
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# Returns the suffix remaining after
|
250
|
+
# +self.take_while(&block)+.
|
251
|
+
#
|
252
|
+
# @return [Consable] the suffix of the elements of +self+.
|
253
|
+
def drop_while(&block)
|
254
|
+
if empty?
|
255
|
+
empty
|
256
|
+
else
|
257
|
+
if yield(head)
|
258
|
+
tail.drop_while(&block)
|
259
|
+
else
|
260
|
+
self
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
# Returns the elements in +self+ for which the given block evaluates to
|
266
|
+
# true.
|
267
|
+
#
|
268
|
+
# @return [Consable] the elements that satisfies the condition.
|
269
|
+
def filter
|
270
|
+
foldr(empty) { |x, xs|
|
271
|
+
if yield(x)
|
272
|
+
Cons(x, xs)
|
273
|
+
else
|
274
|
+
xs
|
275
|
+
end
|
276
|
+
}
|
277
|
+
end
|
278
|
+
|
279
|
+
# Takes zero or more lists and returns a new +Consable+ object in which
|
280
|
+
# each element is an array of the corresponding elements of +self+ and
|
281
|
+
# the input +Consable+ objects.
|
282
|
+
#
|
283
|
+
# @param [Array<Consable>] xss the input +Consable+ objects.
|
284
|
+
# @return [Consable] the new +Consable+ object.
|
285
|
+
def zip(*xss)
|
286
|
+
if empty?
|
287
|
+
empty
|
288
|
+
else
|
289
|
+
heads = xss.map { |xs| xs.empty? ? nil : xs.head }
|
290
|
+
tails = xss.map { |xs| xs.empty? ? empty : xs.tail }
|
291
|
+
Cons([head, *heads], tail.zip(*tails))
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# Takes zero or more +Consable+ objects and returns the +Consable+
|
296
|
+
# object obtained by applying the given block to an array of the
|
297
|
+
# corresponding elements of +self+ and the input +Consable+ objects.
|
298
|
+
# +xs.zip_with(*yss, &block)+ is equivalent to
|
299
|
+
# +xs.zip(*yss).map(&block)+.
|
300
|
+
#
|
301
|
+
# @param [Array<Consable>] xss the input +Consable+ objects.
|
302
|
+
# @return [Consable] the new +Consable+ object.
|
303
|
+
def zip_with(*xss, &block)
|
304
|
+
if empty?
|
305
|
+
empty
|
306
|
+
else
|
307
|
+
heads = xss.map { |xs| xs.null? ? nil : xs.head }
|
308
|
+
tails = xss.map { |xs| xs.null? ? empty : xs.tail }
|
309
|
+
Cons(yield(head, *heads), tail.zip_with(*tails, &block))
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
protected
|
314
|
+
|
315
|
+
def prepend_to_all(sep)
|
316
|
+
if empty?
|
317
|
+
empty
|
318
|
+
else
|
319
|
+
Cons(sep, Cons(head, tail.prepend_to_all(sep)))
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
def nonempty_subsequences
|
324
|
+
if empty?
|
325
|
+
empty
|
326
|
+
else
|
327
|
+
yss = tail.nonempty_subsequences.foldr(empty) { |xs, xss|
|
328
|
+
Cons(xs, Cons(Cons(head, xs), xss))
|
329
|
+
}
|
330
|
+
Cons(Cons(head, empty), yss)
|
331
|
+
end
|
332
|
+
end
|
333
|
+
|
334
|
+
private
|
335
|
+
|
336
|
+
def empty
|
337
|
+
self.class.empty
|
338
|
+
end
|
339
|
+
|
340
|
+
def Cons(x, y)
|
341
|
+
y.cons(x)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|