laziest 0.0.1
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.
- 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: []
|