hamster 0.1.16 → 0.1.17
Sign up to get free protection for your applications and to get access to all the features.
- data/History.rdoc +30 -0
- data/README.rdoc +12 -6
- data/lib/hamster/hash.rb +1 -0
- data/lib/hamster/list.rb +55 -38
- data/lib/hamster/set.rb +12 -0
- data/lib/hamster/version.rb +1 -1
- data/spec/hamster/hash/empty_spec.rb +12 -8
- data/spec/hamster/list/all_spec.rb +3 -7
- data/spec/hamster/list/any_spec.rb +3 -7
- data/spec/hamster/list/append_spec.rb +2 -6
- data/spec/hamster/list/construction_spec.rb +42 -6
- data/spec/hamster/list/cycle_spec.rb +51 -0
- data/spec/hamster/list/drop_spec.rb +2 -6
- data/spec/hamster/list/drop_while_spec.rb +2 -6
- data/spec/hamster/list/each_spec.rb +3 -7
- data/spec/hamster/list/empty_spec.rb +16 -12
- data/spec/hamster/list/eql_spec.rb +4 -9
- data/spec/hamster/list/filter_spec.rb +2 -6
- data/spec/hamster/list/find_spec.rb +2 -6
- data/spec/hamster/list/grep_spec.rb +83 -0
- data/spec/hamster/list/include_spec.rb +2 -6
- data/spec/hamster/list/inspect_spec.rb +2 -6
- data/spec/hamster/list/map_spec.rb +2 -6
- data/spec/hamster/list/maximum_spec.rb +3 -7
- data/spec/hamster/list/minimum_spec.rb +3 -7
- data/spec/hamster/list/none_spec.rb +2 -6
- data/spec/hamster/list/one_spec.rb +3 -7
- data/spec/hamster/list/partition_spec.rb +10 -14
- data/spec/hamster/list/reduce_spec.rb +2 -6
- data/spec/hamster/list/reject_spec.rb +2 -6
- data/spec/hamster/list/reverse_spec.rb +2 -6
- data/spec/hamster/list/size_spec.rb +2 -6
- data/spec/hamster/list/split_at_spec.rb +64 -0
- data/spec/hamster/list/take_spec.rb +2 -6
- data/spec/hamster/list/take_while_spec.rb +2 -6
- data/spec/hamster/list/to_a_spec.rb +2 -6
- data/spec/hamster/list/to_ary_spec.rb +2 -6
- data/spec/hamster/list/zip_spec.rb +50 -0
- data/spec/hamster/set/empty_spec.rb +20 -8
- data/spec/hamster/set/grep_spec.rb +62 -0
- data/spec/hamster/set/to_list.rb +46 -0
- data/spec/hamster/set/uniq_spec.rb +23 -0
- metadata +9 -2
data/History.rdoc
CHANGED
@@ -1,3 +1,33 @@
|
|
1
|
+
=== 0.1.17 / 2010-01-02
|
2
|
+
|
3
|
+
* Alias #empty? as #null?
|
4
|
+
|
5
|
+
* Implement List#split_at.
|
6
|
+
|
7
|
+
* Implement List.iterate.
|
8
|
+
|
9
|
+
* Implement List#cycle.
|
10
|
+
|
11
|
+
* Implement List.replicate.
|
12
|
+
|
13
|
+
* Implement List.repeat.
|
14
|
+
|
15
|
+
* Simplify List#take.
|
16
|
+
|
17
|
+
* Simplify any?, all?, and none?
|
18
|
+
|
19
|
+
* Re-write List#one? to be less complex.
|
20
|
+
|
21
|
+
* Add Set#to_list.
|
22
|
+
|
23
|
+
* Implement List#zip.
|
24
|
+
|
25
|
+
* Implement Set#grep.
|
26
|
+
|
27
|
+
* Implement Set#uniq (aliased as #nub).
|
28
|
+
|
29
|
+
* Implement List#grep.
|
30
|
+
|
1
31
|
=== 0.1.16 / 2009-12-30
|
2
32
|
|
3
33
|
* Ensure streams cache results.
|
data/README.rdoc
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
Hamster started out as an implementation of Hash Array Mapped Trees (HAMT) for Ruby (see http://lamp.epfl.ch/papers/idealhashtrees.pdf) and has since expanded to include implementations of other Persistent Data Structures (see http://en.wikipedia.org/wiki/Persistent_data_structure) such as Sets, Lists, Stacks, etc.
|
4
4
|
|
5
|
-
==
|
5
|
+
== What are persistent data structures?
|
6
6
|
|
7
7
|
Persistent data structures have a really neat property: very efficient copy-on-write operations. That allows you to create immutable data-structures that only need copying when something changes. For example:
|
8
8
|
|
@@ -14,7 +14,7 @@ Persistent data structures have a really neat property: very efficient copy-on-w
|
|
14
14
|
|
15
15
|
== Double Huh? That's not much use!
|
16
16
|
|
17
|
-
Whoops!
|
17
|
+
Whoops! Unlike Ruby's standard library, each call to <tt>Hamster::hash#put</tt> creates an efficient copy containing the modifications, leaving the original unmodified. Thus, all Hamster classes follow Command-Query-Seperation (see http://martinfowler.com/bliki/CommandQuerySeparation.html) and return the modified copy of themselves after any mutating operation. Let's try that again:
|
18
18
|
|
19
19
|
original = Hamster.hash
|
20
20
|
copy = original.put("Name", "Simon")
|
@@ -33,9 +33,9 @@ The same goes for <tt>#remove</tt>:
|
|
33
33
|
|
34
34
|
== Oh, I get it. Cool. But I still don't understand why I should care?
|
35
35
|
|
36
|
-
As mentioned earlier, persistent data structures perform a copy whenever they are modified meaning there is never any chance that two threads could be modifying the same instance at any one time. And, because they are very efficient copies, you don't need to worry about using up gobs of
|
36
|
+
As mentioned earlier, persistent data structures perform a copy whenever they are modified meaning there is never any chance that two threads could be modifying the same instance at any one time. And, because they are very efficient copies, you don't need to worry about using up gobs of memory in the process. Moreover, because they're immutable, you can pass them around between objects, methods, and functions and never worry about data corruption; no more defensive calls to <tt>collection.dup</tt>!
|
37
37
|
|
38
|
-
|
38
|
+
For an interesting read on why immutability is a good thing, take a look at Matthias Felleisen's Function Objects presnetation (http://www.ccs.neu.edu/home/matthias/Presentations/ecoop2004.pdf).
|
39
39
|
|
40
40
|
== OK, that sounds mildly interesting. What's the downside--there's always a downside?
|
41
41
|
|
@@ -127,6 +127,12 @@ Besides <tt>Hamster.list</tt> there are other ways to construct lists:
|
|
127
127
|
|
128
128
|
<tt>Hamster.stream { ... }</tt> allows you to creates infinite lists. Each time a new value is required, the supplied block is called.
|
129
129
|
|
130
|
+
<tt>Hamster.repeat(x)</tt> creates an infinite list with x the value for every element.
|
131
|
+
|
132
|
+
<tt>Hamster.replicate(n, x)</tt> creates a list of size n with x the value for every element.
|
133
|
+
|
134
|
+
<tt>Hamster.iterate(x) { ... }</tt> creates an infinite list where the first item is calculated by applying the block on the initial argument, the second item by applying the function on the previous result and so on.
|
135
|
+
|
130
136
|
You also get <tt>Enumerable#to_list</tt> so you can slowly transition from built-in collection classes to Hamster.
|
131
137
|
|
132
138
|
And finally, you get <tt>IO#to_list</tt> allowing you to lazily processes huge files. For example, imagine the following code to process a 100MB file:
|
@@ -159,6 +165,6 @@ How is this even possible? It's possible because <tt>IO#to_list</tt> creates a l
|
|
159
165
|
|
160
166
|
Hamster started out as a spike to prove a point and has since morphed into something I actually use. My primary concern has been to round out the functionality with good test coverage and clean, readable code.
|
161
167
|
|
162
|
-
Performance is pretty good--especially with lazy lists--but there are some things which unfortunately had to be converted from recursive to iterative due to a lack of Tail-Call-Optimisation in Ruby, making them a little less readable than I would otherwise have preferred.
|
168
|
+
Performance is pretty good--especially with lazy lists--but there are some things which unfortunately had to be converted from recursive to iterative due to a lack of Tail-Call-Optimisation in Ruby, making them a little less readable, and a little more memory hungry, than I would otherwise have preferred.
|
163
169
|
|
164
|
-
Documentation is sparse but I've tried as best I can to write specs that
|
170
|
+
Documentation is sparse but I've tried as best I can to write specs that read as documentation. I've also tried to alias methods as their <tt>Enumerable</tt> equivalents where possible to make it easier for people to migrate code.
|
data/lib/hamster/hash.rb
CHANGED
data/lib/hamster/list.rb
CHANGED
@@ -21,6 +21,18 @@ module Hamster
|
|
21
21
|
end
|
22
22
|
alias_method :range, :interval
|
23
23
|
|
24
|
+
def repeat(item)
|
25
|
+
Sequence.new(item)
|
26
|
+
end
|
27
|
+
|
28
|
+
def replicate(number, item)
|
29
|
+
Sequence.new(item).take(number)
|
30
|
+
end
|
31
|
+
|
32
|
+
def iterate(item, &block)
|
33
|
+
Stream.new(item) { iterate(yield(item), &block) }
|
34
|
+
end
|
35
|
+
|
24
36
|
end
|
25
37
|
|
26
38
|
module List
|
@@ -28,6 +40,7 @@ module Hamster
|
|
28
40
|
def empty?
|
29
41
|
false
|
30
42
|
end
|
43
|
+
alias_method :null?, :empty?
|
31
44
|
|
32
45
|
def size
|
33
46
|
reduce(0) { |memo, item| memo.succ }
|
@@ -100,11 +113,8 @@ module Hamster
|
|
100
113
|
end
|
101
114
|
|
102
115
|
def take(number)
|
103
|
-
|
104
|
-
|
105
|
-
else
|
106
|
-
EmptyList
|
107
|
-
end
|
116
|
+
return EmptyList unless number > 0
|
117
|
+
Stream.new(head) { tail.take(number - 1) }
|
108
118
|
end
|
109
119
|
|
110
120
|
def drop(number)
|
@@ -123,52 +133,33 @@ module Hamster
|
|
123
133
|
alias_method :member?, :include?
|
124
134
|
|
125
135
|
def any?
|
126
|
-
|
127
|
-
|
128
|
-
else
|
129
|
-
each { |item| return true if item }
|
130
|
-
end
|
136
|
+
return any? { |item| item } unless block_given?
|
137
|
+
each { |item| return true if yield(item) }
|
131
138
|
false
|
132
139
|
end
|
133
140
|
alias_method :exist?, :any?
|
134
141
|
alias_method :exists?, :any?
|
135
142
|
|
136
143
|
def all?
|
137
|
-
|
138
|
-
|
139
|
-
else
|
140
|
-
each { |item| return false unless item }
|
141
|
-
end
|
144
|
+
return all? { |item| item } unless block_given?
|
145
|
+
each { |item| return false unless yield(item) }
|
142
146
|
true
|
143
147
|
end
|
144
148
|
|
145
149
|
def none?
|
146
|
-
|
147
|
-
|
148
|
-
else
|
149
|
-
each { |item| return false if item }
|
150
|
-
end
|
150
|
+
return none? { |item| item } unless block_given?
|
151
|
+
each { |item| return false if yield(item) }
|
151
152
|
true
|
152
153
|
end
|
153
154
|
|
154
|
-
def one?
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
found_one = true
|
161
|
-
end
|
162
|
-
end
|
163
|
-
else
|
164
|
-
each do |item|
|
165
|
-
if item
|
166
|
-
return false if found_one
|
167
|
-
found_one = true
|
168
|
-
end
|
169
|
-
end
|
155
|
+
def one?(&block)
|
156
|
+
return one? { |item| item } unless block_given?
|
157
|
+
list = self
|
158
|
+
while !list.empty?
|
159
|
+
return list.tail.none?(&block) if yield(list.head)
|
160
|
+
list = list.tail
|
170
161
|
end
|
171
|
-
|
162
|
+
false
|
172
163
|
end
|
173
164
|
|
174
165
|
def find
|
@@ -205,6 +196,22 @@ module Hamster
|
|
205
196
|
end
|
206
197
|
alias_method :max, :maximum
|
207
198
|
|
199
|
+
def grep(pattern, &block)
|
200
|
+
filter { |item| pattern === item }.map(&block)
|
201
|
+
end
|
202
|
+
|
203
|
+
def zip(other)
|
204
|
+
Stream.new(EmptyList.cons(other.head).cons(head)) { tail.zip(other.tail) }
|
205
|
+
end
|
206
|
+
|
207
|
+
def cycle
|
208
|
+
Stream.new(head) { tail.append(self.cycle) }
|
209
|
+
end
|
210
|
+
|
211
|
+
def split_at(number)
|
212
|
+
EmptyList.cons(drop(number)).cons(take(number))
|
213
|
+
end
|
214
|
+
|
208
215
|
def eql?(other)
|
209
216
|
return false unless other.is_a?(List)
|
210
217
|
|
@@ -270,7 +277,7 @@ module Hamster
|
|
270
277
|
|
271
278
|
attr_reader :head, :tail
|
272
279
|
|
273
|
-
def initialize(head, tail)
|
280
|
+
def initialize(head, tail = self)
|
274
281
|
@head = head
|
275
282
|
@tail = tail
|
276
283
|
end
|
@@ -317,6 +324,7 @@ module Hamster
|
|
317
324
|
def empty?
|
318
325
|
true
|
319
326
|
end
|
327
|
+
alias_method :null?, :empty?
|
320
328
|
|
321
329
|
def map
|
322
330
|
self
|
@@ -349,6 +357,15 @@ module Hamster
|
|
349
357
|
alias_method :cat, :append
|
350
358
|
alias_method :+, :append
|
351
359
|
|
360
|
+
def zip(other)
|
361
|
+
return super unless other.empty?
|
362
|
+
self
|
363
|
+
end
|
364
|
+
|
365
|
+
def cycle
|
366
|
+
self
|
367
|
+
end
|
368
|
+
|
352
369
|
end
|
353
370
|
|
354
371
|
end
|
data/lib/hamster/set.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'hamster/trie'
|
2
|
+
require 'hamster/list'
|
2
3
|
|
3
4
|
module Hamster
|
4
5
|
|
@@ -15,6 +16,7 @@ module Hamster
|
|
15
16
|
def empty?
|
16
17
|
@trie.empty?
|
17
18
|
end
|
19
|
+
alias_method :null?, :empty?
|
18
20
|
|
19
21
|
def size
|
20
22
|
@trie.size
|
@@ -114,6 +116,10 @@ module Hamster
|
|
114
116
|
true
|
115
117
|
end
|
116
118
|
|
119
|
+
def grep(pattern, &block)
|
120
|
+
filter { |item| pattern === item }.map(&block)
|
121
|
+
end
|
122
|
+
|
117
123
|
def eql?(other)
|
118
124
|
other.is_a?(self.class) && @trie.eql?(other.instance_eval{@trie})
|
119
125
|
end
|
@@ -123,12 +129,18 @@ module Hamster
|
|
123
129
|
self
|
124
130
|
end
|
125
131
|
alias_method :clone, :dup
|
132
|
+
alias_method :uniq, :dup
|
133
|
+
alias_method :nub, :dup
|
126
134
|
|
127
135
|
def to_a
|
128
136
|
reduce([]) { |a, item| a << item }
|
129
137
|
end
|
130
138
|
alias_method :entries, :to_a
|
131
139
|
|
140
|
+
def to_list
|
141
|
+
reduce(Hamster.list) { |list, item| list.cons(item) }
|
142
|
+
end
|
143
|
+
|
132
144
|
end
|
133
145
|
|
134
146
|
end
|
data/lib/hamster/version.rb
CHANGED
@@ -4,16 +4,20 @@ require 'hamster/hash'
|
|
4
4
|
|
5
5
|
describe Hamster::Hash do
|
6
6
|
|
7
|
-
|
7
|
+
[:empty?, :null?].each do |method|
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
[
|
12
|
-
|
13
|
-
|
9
|
+
describe "##{method}" do
|
10
|
+
|
11
|
+
[
|
12
|
+
[[], true],
|
13
|
+
[["A" => "aye"], false],
|
14
|
+
[["A" => "aye", "B" => "bee", "C" => "see"], false],
|
15
|
+
].each do |pairs, result|
|
16
|
+
|
17
|
+
it "returns #{result} for #{pairs.inspect}" do
|
18
|
+
Hamster.hash(*pairs).send(method).should == result
|
19
|
+
end
|
14
20
|
|
15
|
-
it "returns #{result} for #{pairs.inspect}" do
|
16
|
-
Hamster.hash(*pairs).empty?.should == result
|
17
21
|
end
|
18
22
|
|
19
23
|
end
|
@@ -8,20 +8,16 @@ describe Hamster::List do
|
|
8
8
|
|
9
9
|
describe "doesn't run out of stack space on a really big" do
|
10
10
|
|
11
|
-
before do
|
12
|
-
@interval = Hamster.interval(0, 10000)
|
13
|
-
end
|
14
|
-
|
15
11
|
it "stream" do
|
16
|
-
@list =
|
12
|
+
@list = Hamster.interval(0, 10000)
|
17
13
|
end
|
18
14
|
|
19
15
|
it "list" do
|
20
|
-
@list =
|
16
|
+
@list = (0..10000).reduce(Hamster.list) { |list, i| list.cons(i) }
|
21
17
|
end
|
22
18
|
|
23
19
|
after do
|
24
|
-
@
|
20
|
+
@list.all? { true }
|
25
21
|
end
|
26
22
|
|
27
23
|
end
|
@@ -10,20 +10,16 @@ describe Hamster::List do
|
|
10
10
|
|
11
11
|
describe "doesn't run out of stack space on a really big" do
|
12
12
|
|
13
|
-
before do
|
14
|
-
@interval = Hamster.interval(0, 10000)
|
15
|
-
end
|
16
|
-
|
17
13
|
it "stream" do
|
18
|
-
@list =
|
14
|
+
@list = Hamster.interval(0, 10000)
|
19
15
|
end
|
20
16
|
|
21
17
|
it "list" do
|
22
|
-
@list =
|
18
|
+
@list = (0..10000).reduce(Hamster.list) { |list, i| list.cons(i) }
|
23
19
|
end
|
24
20
|
|
25
21
|
after do
|
26
|
-
@
|
22
|
+
@list.any? { false }
|
27
23
|
end
|
28
24
|
|
29
25
|
end
|
@@ -10,16 +10,12 @@ describe Hamster::List do
|
|
10
10
|
|
11
11
|
describe "doesn't run out of stack space on a really big" do
|
12
12
|
|
13
|
-
before do
|
14
|
-
@interval = Hamster.interval(0, 10000)
|
15
|
-
end
|
16
|
-
|
17
13
|
it "stream" do
|
18
|
-
@a = @b =
|
14
|
+
@a = @b = Hamster.interval(0, 10000)
|
19
15
|
end
|
20
16
|
|
21
17
|
it "list" do
|
22
|
-
@a = @b =
|
18
|
+
@a = @b = (0..10000).reduce(Hamster.list) { |list, i| list.cons(i) }
|
23
19
|
end
|
24
20
|
|
25
21
|
after do
|
@@ -45,11 +45,11 @@ describe Hamster do
|
|
45
45
|
describe "with no block" do
|
46
46
|
|
47
47
|
before do
|
48
|
-
@
|
48
|
+
@list = Hamster.stream
|
49
49
|
end
|
50
50
|
|
51
51
|
it "returns an empty list" do
|
52
|
-
@
|
52
|
+
@list.should == Hamster.list
|
53
53
|
end
|
54
54
|
|
55
55
|
end
|
@@ -58,11 +58,11 @@ describe Hamster do
|
|
58
58
|
|
59
59
|
before do
|
60
60
|
count = 0
|
61
|
-
@
|
61
|
+
@list = Hamster.stream { count += 1 }
|
62
62
|
end
|
63
63
|
|
64
64
|
it "repeatedly calls the block" do
|
65
|
-
@
|
65
|
+
@list.take(5).should == Hamster.list(1, 2, 3, 4, 5)
|
66
66
|
end
|
67
67
|
|
68
68
|
end
|
@@ -74,15 +74,51 @@ describe Hamster do
|
|
74
74
|
describe ".#{method}" do
|
75
75
|
|
76
76
|
before do
|
77
|
-
@
|
77
|
+
@list = Hamster.send(method, "A", "D")
|
78
78
|
end
|
79
79
|
|
80
80
|
it "is equivalent to a list with explicit values" do
|
81
|
-
@
|
81
|
+
@list.should == Hamster.list("A", "B", "C", "D")
|
82
82
|
end
|
83
83
|
|
84
84
|
end
|
85
85
|
|
86
86
|
end
|
87
87
|
|
88
|
+
describe ".repeat" do
|
89
|
+
|
90
|
+
before do
|
91
|
+
@list = Hamster.repeat("A")
|
92
|
+
end
|
93
|
+
|
94
|
+
it "returns an infinite list with specified value for each element" do
|
95
|
+
@list.take(5).should == Hamster.list("A", "A", "A", "A", "A")
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
describe ".replicate" do
|
101
|
+
|
102
|
+
before do
|
103
|
+
@list = Hamster.replicate(5, "A")
|
104
|
+
end
|
105
|
+
|
106
|
+
it "returns a list with the specified value repeated the specified number of times" do
|
107
|
+
@list.should == Hamster.list("A", "A", "A", "A", "A")
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|
111
|
+
|
112
|
+
describe ".iterate" do
|
113
|
+
|
114
|
+
before do
|
115
|
+
@list = Hamster.iterate(1) { |item| item * 2 }
|
116
|
+
end
|
117
|
+
|
118
|
+
it "returns an infinite list where the first item is calculated by applying the block on the initial argument, the second item by applying the function on the previous result and so on" do
|
119
|
+
@list.take(10).should == Hamster.list(1, 2, 4, 8, 16, 32, 64, 128, 256, 512)
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
|
88
124
|
end
|