laziest 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +83 -0
- data/Rakefile +1 -0
- data/laziest.gemspec +31 -0
- data/lib/laziest.rb +14 -0
- data/lib/laziest/array.rb +109 -0
- data/lib/laziest/counter.rb +44 -0
- data/lib/laziest/extensions.rb +157 -0
- data/lib/laziest/group.rb +93 -0
- data/lib/laziest/minmax.rb +105 -0
- data/lib/laziest/mutex_util.rb +13 -0
- data/lib/laziest/version.rb +3 -0
- metadata +117 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 29f110db9af135c7252ca69456131c27bc35e299
|
4
|
+
data.tar.gz: 17b87349963fd750fce3f8c34b80db66d1eeac65
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: b92d11825b05e450eaf6ffe472c03f2400d7e8d481b8e0eed9aa394ca9eeb743ddda5c4397a648c29d6031ad5be194a1e299b16e50cbe173a0f147776ab95ab3
|
7
|
+
data.tar.gz: 13be6da5325bcef78f8da3580bd926903f9d4f378d47445abf6c2d876c388de4f039c9b2b81483474be22d7ba66eae8bc9d53c847acc6d4b6f6b4e78b3404c82
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 TODO: Write your name
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
# Laziest
|
2
|
+
|
3
|
+
Extends Enumerable#lazy (aka Enumerator::Lazy) with additional
|
4
|
+
opportunities for lazy and especially *partial* evaluation.
|
5
|
+
For example:
|
6
|
+
|
7
|
+
(1..Float::INFINITY).lazy.count > 10
|
8
|
+
|
9
|
+
evaluates almost instantaneously.
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Add this line to your application's Gemfile:
|
14
|
+
|
15
|
+
gem 'laziest'
|
16
|
+
|
17
|
+
And then execute:
|
18
|
+
|
19
|
+
$ bundle
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
$ gem install laziest
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
Simply add this before working with any lazy enumerators:
|
28
|
+
|
29
|
+
require 'laziest'
|
30
|
+
|
31
|
+
This will extend Enumerator::Lazy (and the Enumerable#lazy method)
|
32
|
+
to have lazier versions of the following methods:
|
33
|
+
|
34
|
+
* chunk
|
35
|
+
* count
|
36
|
+
* entries
|
37
|
+
* group_by
|
38
|
+
* max
|
39
|
+
* min
|
40
|
+
* minmax
|
41
|
+
* partition
|
42
|
+
* slice_before
|
43
|
+
* to_a
|
44
|
+
|
45
|
+
The somewhat ambitious goal is to defer as much as possible, while maintaining
|
46
|
+
full compatibility with the existing API. Some more examples:
|
47
|
+
|
48
|
+
(1..Float::INFINITY).lazy.group_by{|x| x.to_s.length}[3].first(2) # => [100, 101]
|
49
|
+
Prime.lazy.max > 1000 # => true
|
50
|
+
Prime.lazy.partition{|x| x%10 == 1}.all? {|x| x.count > 100} # => true
|
51
|
+
|
52
|
+
As with any self-respecting Ruby laziness, proxy classes
|
53
|
+
(specifically, Promises from the promise ("promising future") gem)
|
54
|
+
are abused, to ensure that traditional, non-lazy usage is correct.
|
55
|
+
|
56
|
+
(1..5).lazy.group_by(&:even?) # => {false=>[1, 3, 5], true=>[2, 4]}
|
57
|
+
|
58
|
+
Not everything is as lazy as possible yet, but anything not lazy should still
|
59
|
+
work, eventually, on anything finite.
|
60
|
+
|
61
|
+
Warning: Be careful in IRB! Remember that IRB calls #inspect on the result
|
62
|
+
of any expression you type. This will almost always force a complete
|
63
|
+
evaluation. For example, if you type this:
|
64
|
+
|
65
|
+
evens, odds = (1..Float::INFINITY).lazy.partition(&:even?)
|
66
|
+
|
67
|
+
This will attempt to #inspect the resulting arrays, and you have created an
|
68
|
+
infinite loop. You can avoid this by adding a useless expression to the end:
|
69
|
+
|
70
|
+
evens, odds = (1..Float::INFINITY).lazy.partition(&:even?); nil
|
71
|
+
evens.first(10)
|
72
|
+
|
73
|
+
Of course, abusing one-liners is the easiest way to avoid this:
|
74
|
+
|
75
|
+
(1..Float::INFINITY).lazy.group_by(&:even?)[true].first(10)
|
76
|
+
|
77
|
+
## Contributing
|
78
|
+
|
79
|
+
1. Fork it
|
80
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
81
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
82
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
83
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/laziest.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'laziest/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'laziest'
|
8
|
+
spec.version = Laziest::VERSION
|
9
|
+
spec.authors = ['David Masover']
|
10
|
+
spec.email = ['masover@iastate.edu']
|
11
|
+
spec.description = 'The laziest possible enumerables and enumerators'
|
12
|
+
spec.summary = <<-END
|
13
|
+
When there's just no O(1) way to compute something, this gem provides
|
14
|
+
both promises (lazy evaluation) and partial evaluation, along with
|
15
|
+
implicit, softref-based memoization. For example, (foo.lazy.count > 5) will
|
16
|
+
invoke the iterator at most six times.
|
17
|
+
END
|
18
|
+
spec.homepage = ''
|
19
|
+
spec.license = 'MIT'
|
20
|
+
|
21
|
+
spec.files = `git ls-files`.split($/)
|
22
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
23
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
24
|
+
spec.require_paths = ['lib']
|
25
|
+
|
26
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
27
|
+
spec.add_development_dependency "rake"
|
28
|
+
|
29
|
+
spec.add_dependency 'promise'
|
30
|
+
spec.add_dependency 'soft_reference'
|
31
|
+
end
|
data/lib/laziest.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
autoload :Promise, 'promise'
|
2
|
+
|
3
|
+
module Laziest
|
4
|
+
autoload :VERSION, 'laziest/version'
|
5
|
+
autoload :Counter, 'laziest/counter'
|
6
|
+
autoload :ArrayPromise, 'laziest/array'
|
7
|
+
autoload :Group, 'laziest/group'
|
8
|
+
autoload :MutexUtil, 'laziest/mutex_util'
|
9
|
+
autoload :MinMax, 'laziest/minmax'
|
10
|
+
|
11
|
+
require 'laziest/extensions'
|
12
|
+
|
13
|
+
Enumerator::Lazy.send :include, Extensions::LazyEnumerator
|
14
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
module Laziest
|
2
|
+
# This class is a lazily-evaluated array based on an enumerator.
|
3
|
+
# A contract: The enumerator is guaranteed to never be rewound.
|
4
|
+
class ArrayPromise < Promise
|
5
|
+
# Accepts 'array' to allow a partially-evaluated enumerator/array to initialize.
|
6
|
+
def initialize enumerator, array=[], nil_on_empty = false
|
7
|
+
@array = array
|
8
|
+
@enumerator = enumerator
|
9
|
+
@nil_on_empty = nil_on_empty
|
10
|
+
super() do
|
11
|
+
::Kernel.loop do
|
12
|
+
@array << @enumerator.next
|
13
|
+
end
|
14
|
+
@enumerator = nil
|
15
|
+
# Needed for use in hashes, like group_by
|
16
|
+
(nil_on_empty && @array.empty?) ? nil : @array
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def each(&block)
|
21
|
+
return super if __forced__?
|
22
|
+
return ::Enumerator.new{|y| each {|x| y << x}}.lazy unless ::Kernel.block_given?
|
23
|
+
index = 0
|
24
|
+
::Kernel.loop do
|
25
|
+
__force_to__ index
|
26
|
+
break unless @array.length > index
|
27
|
+
yield @array[index]
|
28
|
+
index += 1
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Array-related laziness
|
33
|
+
def [] index, length=nil
|
34
|
+
return super if __forced__?
|
35
|
+
if length.nil?
|
36
|
+
if index.kind_of? ::Range
|
37
|
+
enum = ::Enumerator.new do |y|
|
38
|
+
index.each do |i|
|
39
|
+
y << self[i]
|
40
|
+
end
|
41
|
+
end
|
42
|
+
ArrayPromise.new enum
|
43
|
+
else
|
44
|
+
__force_to__ index
|
45
|
+
@array[index]
|
46
|
+
end
|
47
|
+
else
|
48
|
+
enum = ::Enumerator.new do |y|
|
49
|
+
i = index
|
50
|
+
length.times do
|
51
|
+
y << self[i]
|
52
|
+
i += 1
|
53
|
+
end
|
54
|
+
end
|
55
|
+
ArrayPromise.new enum
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def __force_to__ index
|
60
|
+
begin
|
61
|
+
@mutex.synchronize do
|
62
|
+
while @array.length <= index
|
63
|
+
@array << @enumerator.next
|
64
|
+
end
|
65
|
+
end
|
66
|
+
rescue ::StopIteration
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def __forced__?
|
71
|
+
if @nil_on_empty && @array.empty?
|
72
|
+
# Check if we're really empty.
|
73
|
+
__force_to__ 1
|
74
|
+
# Either we'll be empty (and finalized as nil), so we'll revert to
|
75
|
+
# calling super which forwards to nil and so on,
|
76
|
+
# or we'll be nonempty (and normal logic proceeds)
|
77
|
+
end
|
78
|
+
!(@result.equal?(NOT_SET) && @error.equal?(NOT_SET))
|
79
|
+
end
|
80
|
+
|
81
|
+
# Force all enumerable methods to be defined in terms of laziness.
|
82
|
+
%w(
|
83
|
+
all? any? chunk collect collect_concat count cycle detect drop
|
84
|
+
drop_while each_cons each_entry each_slice each_with_index
|
85
|
+
each_with_object entries find find_all find_index first flat_map grep
|
86
|
+
group_by include? inject lazy map max max_by member? min min_by minmax
|
87
|
+
minmax_by none? one? partition reduce reject reverse_each select
|
88
|
+
slice_before sort sort_by take take_while zip
|
89
|
+
).map(&:to_sym).each do |name|
|
90
|
+
define_method name do |*args, &block|
|
91
|
+
return super if __forced__?
|
92
|
+
each.lazy.public_send name, *args, &block
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
%w(length size).map(&:to_sym).each do |name|
|
97
|
+
define_method name do
|
98
|
+
return super if __forced__?
|
99
|
+
each.lazy.count
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Except this one, make it a no-op. (Also should prevent infinite recursion.)
|
104
|
+
def to_a
|
105
|
+
return super if __forced__?
|
106
|
+
self
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Laziest
|
2
|
+
class Counter < Promise
|
3
|
+
def initialize enumerator
|
4
|
+
@count = 0
|
5
|
+
@enumerator = enumerator
|
6
|
+
super() do
|
7
|
+
::Kernel.loop do
|
8
|
+
@count += enumerator.next
|
9
|
+
end
|
10
|
+
@count
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def __force_to__ value
|
15
|
+
if @result.equal?(NOT_SET) && @error.equal?(NOT_SET)
|
16
|
+
@mutex.synchronize do
|
17
|
+
if @result.equal?(NOT_SET) && @error.equal?(NOT_SET)
|
18
|
+
begin
|
19
|
+
while @count < value
|
20
|
+
@count += @enumerator.next
|
21
|
+
end
|
22
|
+
rescue ::StopIteration
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
::Kernel.raise(@error) unless @error.equal?(NOT_SET)
|
28
|
+
end
|
29
|
+
|
30
|
+
%w(== eql? equal? <= > <=>).map(&:to_sym).each do |op|
|
31
|
+
define_method op do |value|
|
32
|
+
__force_to__ value+1
|
33
|
+
@count.public_send op, value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
%w(< >=).map(&:to_sym).each do |op|
|
38
|
+
define_method op do |value|
|
39
|
+
__force_to__ value
|
40
|
+
@count.public_send op, value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
module Laziest
|
2
|
+
module Extensions
|
3
|
+
module LazyEnumerator
|
4
|
+
def count
|
5
|
+
enum = Enumerator.new do |y|
|
6
|
+
each do |value|
|
7
|
+
if (!block_given?) || (yield value)
|
8
|
+
y << 1
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
Counter.new enum
|
13
|
+
end
|
14
|
+
|
15
|
+
def entries
|
16
|
+
ArrayPromise.new enum_for :each
|
17
|
+
end
|
18
|
+
alias_method :to_a, :entries
|
19
|
+
|
20
|
+
def group_by &block
|
21
|
+
return enum_for :group_by unless block_given?
|
22
|
+
Group.new enum_for(:each), &block
|
23
|
+
end
|
24
|
+
|
25
|
+
%w(min max).map(&:to_sym).each do |name|
|
26
|
+
klass = MinMax.const_get name.capitalize
|
27
|
+
define_method name do
|
28
|
+
if block_given?
|
29
|
+
# can't handle unnatural ordering
|
30
|
+
super
|
31
|
+
else
|
32
|
+
klass.new enum_for(&:each)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Perfectly functional.
|
38
|
+
# TODO: Optimize by sharing an enumerator. As it stands,
|
39
|
+
# in the worst case, we enumerate everything twice, defeating the
|
40
|
+
# entire point of having a separate 'minmax' method.
|
41
|
+
def minmax
|
42
|
+
if block_given?
|
43
|
+
super
|
44
|
+
else
|
45
|
+
[min, max]
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Another functional-but-stub.
|
50
|
+
# TODO: Optimize away the hash, maybe?
|
51
|
+
def partition
|
52
|
+
return enum_for :partition unless block_given?
|
53
|
+
# Negate, forcing these to actual true/false values
|
54
|
+
groups = group_by{|*args| ! yield *args}
|
55
|
+
# No need to double-negate, the following is just inverted.
|
56
|
+
[groups[false], groups[true]]
|
57
|
+
end
|
58
|
+
|
59
|
+
# Implemented mainly in terms of slice_before.
|
60
|
+
# It's logically very similar, but has similar amounts of extra sugar.
|
61
|
+
def chunk state=nil
|
62
|
+
# Handle state, so we can forget about it.
|
63
|
+
unless state.nil?
|
64
|
+
return chunk {|value| yield value, state}
|
65
|
+
end
|
66
|
+
|
67
|
+
# Even though we could probably do everything with the lazy enum
|
68
|
+
# chaining -- and we do _almost_ everything that way -- we want
|
69
|
+
# to ensure that first_iteration is reset with each run. A brand
|
70
|
+
# new enumerator ensures #rewind and friends work correctly.
|
71
|
+
|
72
|
+
enum = Enumerator.new do |yield_array|
|
73
|
+
prev = nil
|
74
|
+
first_iteration = true
|
75
|
+
|
76
|
+
map {|value|
|
77
|
+
[value, (yield value)]
|
78
|
+
}.slice_before {|value, result|
|
79
|
+
if first_iteration
|
80
|
+
first_iteration = false
|
81
|
+
# return value doesn't matter
|
82
|
+
else
|
83
|
+
# start a new group when this one differs from prev, or is
|
84
|
+
# specifically requested as 'alone'.
|
85
|
+
result != prev || result == :_alone
|
86
|
+
end
|
87
|
+
prev = result
|
88
|
+
}.each do |value, result|
|
89
|
+
yield result unless value.nil? || value == :_separator
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
enum.lazy
|
94
|
+
end
|
95
|
+
|
96
|
+
# Chunk without laziness is actually good enough already for most uses.
|
97
|
+
# Generally, you're chunking a huge stream into small, manageable chunks.
|
98
|
+
# The standard enumerable will stream well enough there -- each chunk
|
99
|
+
# must be evaluated completely, but the entire stream is evaluated
|
100
|
+
# only as needed. The use case here is if any particular chunk is _very_
|
101
|
+
# large, and maybe infinite, but we actually don't need the entire chunk.
|
102
|
+
def slice_before state=nil
|
103
|
+
unless block_given?
|
104
|
+
# state is a pattern
|
105
|
+
return slice_before {|value| state === value}
|
106
|
+
end
|
107
|
+
|
108
|
+
unless state.nil?
|
109
|
+
# state is state, handle it here
|
110
|
+
return slice_before {|value| yield value, state}
|
111
|
+
end
|
112
|
+
|
113
|
+
enum = Enumerator.new do |yield_array|
|
114
|
+
ec = enum_for(:each)
|
115
|
+
array = nil
|
116
|
+
lazy_array = nil
|
117
|
+
|
118
|
+
begin
|
119
|
+
array = [ec.next]
|
120
|
+
rescue StopIteration
|
121
|
+
array = nil
|
122
|
+
end
|
123
|
+
|
124
|
+
loop do
|
125
|
+
break if array.nil?
|
126
|
+
|
127
|
+
array_promise_enum = Enumerator.new do |yield_element|
|
128
|
+
loop do
|
129
|
+
value = nil
|
130
|
+
begin
|
131
|
+
value = ec.next
|
132
|
+
if yield value
|
133
|
+
array = [value]
|
134
|
+
break
|
135
|
+
end
|
136
|
+
rescue StopIteration
|
137
|
+
array = nil
|
138
|
+
lazy_array = nil
|
139
|
+
break
|
140
|
+
end
|
141
|
+
yield_element << value
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
lazy_array = ArrayPromise.new array_promise_enum, array
|
146
|
+
|
147
|
+
yield_array << lazy_array
|
148
|
+
|
149
|
+
lazy_array.__force__
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
enum.lazy
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Laziest
|
4
|
+
class Group < Promise
|
5
|
+
def initialize enumerator, &block
|
6
|
+
@hash = {}
|
7
|
+
@lazy_arrays = {}
|
8
|
+
@buffers = ::Hash.new{|h,k|h[k]=[]}
|
9
|
+
@enumerator = enumerator
|
10
|
+
@group_block = block
|
11
|
+
@enum_mutex = ::Mutex.new
|
12
|
+
super() do
|
13
|
+
# Any existing lazy arrays can simply be forced, since
|
14
|
+
# they're on a different mutex now:
|
15
|
+
@lazy_arrays.each do |arr|
|
16
|
+
arr.__force__
|
17
|
+
end
|
18
|
+
# And just in case there aren't any lazy arrays:
|
19
|
+
::Kernel.loop do
|
20
|
+
val = enumerator.next
|
21
|
+
key = yield val
|
22
|
+
(@hash[key] ||= []) << val
|
23
|
+
end
|
24
|
+
# Strip out anything that turned out to be empty.
|
25
|
+
@hash.delete_if {|k,v| v.empty?}
|
26
|
+
# clear potentially-expensive GC-able stuff.
|
27
|
+
@lazy_arrays = nil
|
28
|
+
@buffers = nil
|
29
|
+
@group_block = nil
|
30
|
+
# safe because we already forced all existing lazy arrays, and we're
|
31
|
+
# holding the global mutex which prevents any new ones from being built
|
32
|
+
@enum_mutex = nil
|
33
|
+
@hash
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def [] key
|
38
|
+
return super unless @result.equal?(NOT_SET) && @error.equal?(NOT_SET)
|
39
|
+
@mutex.synchronize do
|
40
|
+
@enum_mutex.synchronize do
|
41
|
+
# for good measure -- for once, we really _don't_ want to do _any_ of
|
42
|
+
# this once the hash's promise resolves.
|
43
|
+
return super unless @result.equal?(NOT_SET) && @error.equal?(NOT_SET)
|
44
|
+
if @lazy_arrays.has_key?(key)
|
45
|
+
@lazy_arrays[key]
|
46
|
+
else
|
47
|
+
# Note: This enumerator is only ever passed to ArrayPromise.
|
48
|
+
# It is not actually a valid enumerator, as it entirely lacks
|
49
|
+
# support for rewinding.
|
50
|
+
enum = ::Enumerator.new do |yielder|
|
51
|
+
@enum_mutex.synchronize do
|
52
|
+
buffer = @buffers[key]
|
53
|
+
::Kernel.loop do
|
54
|
+
if buffer.empty?
|
55
|
+
# At this point, @enumerator.next may throw StopIteration.
|
56
|
+
# Which will break the loop, which will end our iteration,
|
57
|
+
# which is fine. (A call to our enumerator.each would also
|
58
|
+
# result in a StopIteration.)
|
59
|
+
v = @enumerator.next
|
60
|
+
k = @group_block.call v
|
61
|
+
if k == key
|
62
|
+
# We shouldn't hold the lock when we yield.
|
63
|
+
::Laziest::MutexUtil.unsynchronize @enum_mutex do
|
64
|
+
yielder << v
|
65
|
+
end
|
66
|
+
elsif @hash.has_key? k
|
67
|
+
# Buffer data for the lazy array to read later
|
68
|
+
if @lazy_arrays.has_key? k
|
69
|
+
@buffers[k] << v
|
70
|
+
else
|
71
|
+
@hash[k] << v
|
72
|
+
end
|
73
|
+
else
|
74
|
+
# We know a lazy array doesn't exist unless the hash exists
|
75
|
+
@hash[k] = [v]
|
76
|
+
end
|
77
|
+
else
|
78
|
+
v = buffer.shift
|
79
|
+
::Laziest::MutexUtil.unsynchronize @enum_mutex do
|
80
|
+
yielder << v
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
@hash[key] = [] unless @hash.has_key? key
|
87
|
+
@lazy_arrays[key] = ArrayPromise.new enum, @hash[key], true
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module Laziest
|
2
|
+
# Base class. Conceptually a max, but we can override it
|
3
|
+
# with min/max classes... but only for naturally-ordered elements!
|
4
|
+
# If we allow arbitrary comparators, we have no way of optimizing
|
5
|
+
# attempted comparisons against the max value itself.
|
6
|
+
class MinMax < Promise
|
7
|
+
def initialize enumerator, gtop
|
8
|
+
@enumerator = enumerator
|
9
|
+
# "Greater-Than" operation
|
10
|
+
@gtop = gtop
|
11
|
+
begin
|
12
|
+
@max = @enumerator.next
|
13
|
+
rescue ::StopIteration
|
14
|
+
@is_nil = true
|
15
|
+
end
|
16
|
+
|
17
|
+
super() do
|
18
|
+
::Kernel.loop do
|
19
|
+
value = @enumerator.next
|
20
|
+
if value.public_send @gtop, @max
|
21
|
+
@max = value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
@max
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def __force_until__
|
29
|
+
if @result.equal?(NOT_SET) && @error.equal?(NOT_SET)
|
30
|
+
@mutex.synchronize do
|
31
|
+
if @result.equal?(NOT_SET) && @error.equal?(NOT_SET)
|
32
|
+
unless yield
|
33
|
+
::Kernel.loop do
|
34
|
+
# StopIteration can stop the loop here
|
35
|
+
value = @enumerator.next
|
36
|
+
if value.public_send @gtop, @max
|
37
|
+
@max = value
|
38
|
+
break if yield
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
::Kernel.raise(@error) unless @error.equal?(NOT_SET)
|
46
|
+
end
|
47
|
+
|
48
|
+
%w(== eql? equal?).map(&:to_sym).each do |name|
|
49
|
+
define_method name do |value|
|
50
|
+
# If it's _equal_ to our value, it can't be _greater_ than our value.
|
51
|
+
# So this can lazily return _false_ if the max is _greater_ than
|
52
|
+
# our value. Otherwise, we have to evaluate the whole sequence.
|
53
|
+
__force_until__ { @max.public_send @gtop, value }
|
54
|
+
@max.public_send name, value
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# The individual Min and Max classes provide additional optimizations for
|
59
|
+
# naturally-ordered elements.
|
60
|
+
class Max < MinMax
|
61
|
+
def initialize enumerator
|
62
|
+
super(enumerator, :>)
|
63
|
+
end
|
64
|
+
|
65
|
+
%w(< >=).map(&:to_sym).each do |name|
|
66
|
+
define_method name do |value|
|
67
|
+
# If we find a max at least as large as value, then < is false,
|
68
|
+
# and >= is true. (Otherwise, we must enumerate everything.)
|
69
|
+
__force_until__ { @max >= value }
|
70
|
+
@max.public_send name, value
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
%w(<= <=> >).map(&:to_sym).each do |name|
|
75
|
+
define_method name do |value|
|
76
|
+
# If we find a max _larger_ than a value, then <= is false,
|
77
|
+
# > is true, and <=> can be evaluated directly.
|
78
|
+
__force_until__ { @max > value }
|
79
|
+
@max.public_send name, value
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Inverse of above.
|
85
|
+
class Min < MinMax
|
86
|
+
def initialize enumerator
|
87
|
+
super(enumerator, :<)
|
88
|
+
end
|
89
|
+
|
90
|
+
%w(> <=).map(&:to_sym).each do |name|
|
91
|
+
define_method name do |value|
|
92
|
+
__force_until__ { @max <= value }
|
93
|
+
@max.public_send name, value
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
%w(>= <=> <).map(&:to_sym).each do |name|
|
98
|
+
define_method name do |value|
|
99
|
+
__force_until__ { @max < value }
|
100
|
+
@max.public_send name, value
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
metadata
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: laziest
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- David Masover
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-07-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.3'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.3'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: promise
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: soft_reference
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
description: The laziest possible enumerables and enumerators
|
70
|
+
email:
|
71
|
+
- masover@iastate.edu
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- .gitignore
|
77
|
+
- Gemfile
|
78
|
+
- LICENSE.txt
|
79
|
+
- README.md
|
80
|
+
- Rakefile
|
81
|
+
- laziest.gemspec
|
82
|
+
- lib/laziest.rb
|
83
|
+
- lib/laziest/array.rb
|
84
|
+
- lib/laziest/counter.rb
|
85
|
+
- lib/laziest/extensions.rb
|
86
|
+
- lib/laziest/group.rb
|
87
|
+
- lib/laziest/minmax.rb
|
88
|
+
- lib/laziest/mutex_util.rb
|
89
|
+
- lib/laziest/version.rb
|
90
|
+
homepage: ''
|
91
|
+
licenses:
|
92
|
+
- MIT
|
93
|
+
metadata: {}
|
94
|
+
post_install_message:
|
95
|
+
rdoc_options: []
|
96
|
+
require_paths:
|
97
|
+
- lib
|
98
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - '>='
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
104
|
+
requirements:
|
105
|
+
- - '>='
|
106
|
+
- !ruby/object:Gem::Version
|
107
|
+
version: '0'
|
108
|
+
requirements: []
|
109
|
+
rubyforge_project:
|
110
|
+
rubygems_version: 2.0.3
|
111
|
+
signing_key:
|
112
|
+
specification_version: 4
|
113
|
+
summary: When there's just no O(1) way to compute something, this gem provides both
|
114
|
+
promises (lazy evaluation) and partial evaluation, along with implicit, softref-based
|
115
|
+
memoization. For example, (foo.lazy.count > 5) will invoke the iterator at most
|
116
|
+
six times.
|
117
|
+
test_files: []
|