async-enumerable 0.1.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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.standard.yml +5 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README.md +416 -0
- data/Rakefile +127 -0
- data/benchmark/async_all.yaml +38 -0
- data/benchmark/async_any.yaml +39 -0
- data/benchmark/async_each.yaml +51 -0
- data/benchmark/async_find.yaml +37 -0
- data/benchmark/async_map.yaml +50 -0
- data/benchmark/async_select.yaml +31 -0
- data/benchmark/early_termination/any_early.yaml +17 -0
- data/benchmark/early_termination/any_late.yaml +17 -0
- data/benchmark/early_termination/find_middle.yaml +17 -0
- data/benchmark/size_comparison/map_10.yaml +17 -0
- data/benchmark/size_comparison/map_100.yaml +17 -0
- data/benchmark/size_comparison/map_1000.yaml +20 -0
- data/benchmark/size_comparison/map_10000.yaml +23 -0
- data/docs/reference/README.md +43 -0
- data/docs/reference/concurrency_bounder.md +234 -0
- data/docs/reference/enumerable.md +258 -0
- data/docs/reference/enumerator.md +221 -0
- data/docs/reference/methods/converters.md +97 -0
- data/docs/reference/methods/predicates.md +254 -0
- data/docs/reference/methods/transformers.md +104 -0
- data/lib/async/enumerable/comparable.rb +26 -0
- data/lib/async/enumerable/concurrency_bounder.rb +37 -0
- data/lib/async/enumerable/configurable.rb +140 -0
- data/lib/async/enumerable/methods/aggregators.rb +40 -0
- data/lib/async/enumerable/methods/converters.rb +21 -0
- data/lib/async/enumerable/methods/each.rb +39 -0
- data/lib/async/enumerable/methods/iterators.rb +27 -0
- data/lib/async/enumerable/methods/predicates/all.rb +47 -0
- data/lib/async/enumerable/methods/predicates/any.rb +47 -0
- data/lib/async/enumerable/methods/predicates/find.rb +55 -0
- data/lib/async/enumerable/methods/predicates/find_index.rb +50 -0
- data/lib/async/enumerable/methods/predicates/include.rb +23 -0
- data/lib/async/enumerable/methods/predicates/none.rb +27 -0
- data/lib/async/enumerable/methods/predicates/one.rb +48 -0
- data/lib/async/enumerable/methods/predicates.rb +29 -0
- data/lib/async/enumerable/methods/slicers.rb +34 -0
- data/lib/async/enumerable/methods/transformers/compact.rb +18 -0
- data/lib/async/enumerable/methods/transformers/filter_map.rb +19 -0
- data/lib/async/enumerable/methods/transformers/flat_map.rb +20 -0
- data/lib/async/enumerable/methods/transformers/map.rb +22 -0
- data/lib/async/enumerable/methods/transformers/reject.rb +19 -0
- data/lib/async/enumerable/methods/transformers/select.rb +21 -0
- data/lib/async/enumerable/methods/transformers/sort.rb +18 -0
- data/lib/async/enumerable/methods/transformers/sort_by.rb +19 -0
- data/lib/async/enumerable/methods/transformers/uniq.rb +18 -0
- data/lib/async/enumerable/methods/transformers.rb +35 -0
- data/lib/async/enumerable/methods.rb +26 -0
- data/lib/async/enumerable/version.rb +10 -0
- data/lib/async/enumerable.rb +72 -0
- data/lib/async/enumerator.rb +33 -0
- data/lib/enumerable/async.rb +38 -0
- data/scripts/debug_config.rb +26 -0
- data/scripts/debug_config2.rb +34 -0
- data/scripts/sketch.rb +30 -0
- data/scripts/test_aggregators.rb +66 -0
- data/scripts/test_ancestors.rb +12 -0
- data/scripts/test_async_chaining.rb +30 -0
- data/scripts/test_direct_method_calls.rb +53 -0
- data/scripts/test_example.rb +37 -0
- data/scripts/test_issue_7.rb +69 -0
- data/scripts/test_method_source.rb +15 -0
- metadata +145 -0
@@ -0,0 +1,97 @@
|
|
1
|
+
# Converter Methods
|
2
|
+
|
3
|
+
Converter methods transform async enumerables into other data types, typically arrays.
|
4
|
+
|
5
|
+
## to_a
|
6
|
+
|
7
|
+
Converts the wrapped enumerable to an array.
|
8
|
+
|
9
|
+
This method simply converts the wrapped enumerable to an array without any async processing. Note that async operations like map and select already return arrays internally.
|
10
|
+
|
11
|
+
### Returns
|
12
|
+
`Array` - The wrapped enumerable converted to an array
|
13
|
+
|
14
|
+
### Examples
|
15
|
+
|
16
|
+
```ruby
|
17
|
+
# Basic conversion
|
18
|
+
async_enum = (1..3).async
|
19
|
+
async_enum.to_a # => [1, 2, 3]
|
20
|
+
|
21
|
+
# Converting a Set
|
22
|
+
async_set = Set[1, 2, 3].async
|
23
|
+
async_set.to_a # => [1, 2, 3] (order may vary)
|
24
|
+
|
25
|
+
# After transformations
|
26
|
+
[1, 2, 3].async.map { |n| n * 2 }.to_a # => [2, 4, 6]
|
27
|
+
```
|
28
|
+
|
29
|
+
### Implementation Notes
|
30
|
+
- Uses `enumerable_source` to get the appropriate source
|
31
|
+
- Handles self-referential sources to avoid infinite recursion
|
32
|
+
- Delegates to the source's `to_a` method
|
33
|
+
|
34
|
+
## sync
|
35
|
+
|
36
|
+
Synchronizes the async enumerable back to a regular array. This is an alias for `to_a` that provides a more semantic way to end an async chain and get the results.
|
37
|
+
|
38
|
+
### Returns
|
39
|
+
`Array` - The wrapped enumerable converted to an array
|
40
|
+
|
41
|
+
### Examples
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
# Chaining with sync
|
45
|
+
result = [:foo, :bar].async
|
46
|
+
.map { |sym| fetch_data(sym) }
|
47
|
+
.sync
|
48
|
+
# => [<data for :foo>, <data for :bar>]
|
49
|
+
|
50
|
+
# Alternative to to_a
|
51
|
+
data.async.select { |x| x.valid? }.sync # same as .to_a
|
52
|
+
|
53
|
+
# Complete async pipeline
|
54
|
+
[1, 2, 3, 4, 5].async
|
55
|
+
.map { |n| expensive_operation(n) }
|
56
|
+
.select { |result| result.success? }
|
57
|
+
.map { |result| result.value }
|
58
|
+
.sync # Materializes final results
|
59
|
+
```
|
60
|
+
|
61
|
+
### Why Use sync?
|
62
|
+
|
63
|
+
The `sync` method provides semantic clarity:
|
64
|
+
- `to_a` implies conversion to array format
|
65
|
+
- `sync` implies waiting for async operations to complete and collecting results
|
66
|
+
- Both do the same thing, but `sync` better expresses intent in async contexts
|
67
|
+
|
68
|
+
## Usage Patterns
|
69
|
+
|
70
|
+
### Basic Conversion
|
71
|
+
```ruby
|
72
|
+
# Simple enumerable to array
|
73
|
+
(1..5).async.to_a # => [1, 2, 3, 4, 5]
|
74
|
+
```
|
75
|
+
|
76
|
+
### After Async Operations
|
77
|
+
```ruby
|
78
|
+
# Process data asynchronously, then collect results
|
79
|
+
urls.async
|
80
|
+
.map { |url| fetch_data(url) }
|
81
|
+
.select { |data| data.valid? }
|
82
|
+
.sync # Get final array of valid data
|
83
|
+
```
|
84
|
+
|
85
|
+
### With Custom Enumerables
|
86
|
+
```ruby
|
87
|
+
class MyCollection
|
88
|
+
include Enumerable
|
89
|
+
def each
|
90
|
+
yield 1
|
91
|
+
yield 2
|
92
|
+
yield 3
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
MyCollection.new.async.to_a # => [1, 2, 3]
|
97
|
+
```
|
@@ -0,0 +1,254 @@
|
|
1
|
+
# Predicate Methods
|
2
|
+
|
3
|
+
Predicate methods test elements in the enumerable and return boolean values. All async predicate methods support early termination - they stop processing as soon as the result is determined.
|
4
|
+
|
5
|
+
## any?
|
6
|
+
|
7
|
+
Asynchronously checks if any element satisfies the given condition.
|
8
|
+
|
9
|
+
Executes the block for each element in parallel and returns true as soon as any element returns a truthy value. Short-circuits and stops processing remaining elements once a match is found.
|
10
|
+
|
11
|
+
### Parameters
|
12
|
+
- `pattern` (optional): Pattern to match against elements
|
13
|
+
- `&block`: Block to test each element
|
14
|
+
|
15
|
+
### Returns
|
16
|
+
`Boolean` - true if any element satisfies the condition, false otherwise
|
17
|
+
|
18
|
+
### Examples
|
19
|
+
|
20
|
+
```ruby
|
21
|
+
# Check if any number is negative
|
22
|
+
[1, 2, -3].async.any? { |n| n < 0 } # => true (stops after -3)
|
23
|
+
[1, 2, 3].async.any? { |n| n < 0 } # => false
|
24
|
+
|
25
|
+
# With API calls
|
26
|
+
servers.async.any? { |server| server_responding?(server) }
|
27
|
+
# Checks all servers in parallel, returns true on first response
|
28
|
+
```
|
29
|
+
|
30
|
+
### Implementation Notes
|
31
|
+
- Uses `Concurrent::AtomicBoolean` for thread-safe early termination
|
32
|
+
- Delegates pattern/no-block cases to wrapped enumerable to avoid break issues
|
33
|
+
- Stops barrier execution as soon as a match is found
|
34
|
+
|
35
|
+
## all?
|
36
|
+
|
37
|
+
Asynchronously checks if all elements satisfy the given condition.
|
38
|
+
|
39
|
+
Executes the block for each element in parallel and returns false as soon as any element returns a falsy value. Short-circuits and stops processing remaining elements once a non-match is found.
|
40
|
+
|
41
|
+
### Parameters
|
42
|
+
- `pattern` (optional): Pattern to match against elements
|
43
|
+
- `&block`: Block to test each element
|
44
|
+
|
45
|
+
### Returns
|
46
|
+
`Boolean` - true if all elements satisfy the condition, false otherwise
|
47
|
+
|
48
|
+
### Examples
|
49
|
+
|
50
|
+
```ruby
|
51
|
+
# Check if all numbers are positive
|
52
|
+
[1, 2, 3].async.all? { |n| n > 0 } # => true
|
53
|
+
[1, -2, 3].async.all? { |n| n > 0 } # => false (stops after -2)
|
54
|
+
|
55
|
+
# With validation
|
56
|
+
forms.async.all? { |form| validate_form(form) }
|
57
|
+
# Validates all forms in parallel, returns false on first invalid
|
58
|
+
```
|
59
|
+
|
60
|
+
### Implementation Notes
|
61
|
+
- Uses `Concurrent::AtomicBoolean` to track if any element fails the test
|
62
|
+
- Delegates pattern/no-block cases to wrapped enumerable
|
63
|
+
- Stops barrier execution as soon as a non-match is found
|
64
|
+
|
65
|
+
## none?
|
66
|
+
|
67
|
+
Asynchronously checks if no elements satisfy the given condition.
|
68
|
+
|
69
|
+
Executes the block for each element in parallel and returns false as soon as any element returns a truthy value. Short-circuits and stops processing remaining elements once a match is found.
|
70
|
+
|
71
|
+
### Parameters
|
72
|
+
- `pattern` (optional): Pattern to match against elements
|
73
|
+
- `&block`: Block to test each element
|
74
|
+
|
75
|
+
### Returns
|
76
|
+
`Boolean` - true if no elements satisfy the condition, false otherwise
|
77
|
+
|
78
|
+
### Examples
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
# Check if no numbers are negative
|
82
|
+
[1, 2, 3].async.none? { |n| n < 0 } # => true
|
83
|
+
[1, -2, 3].async.none? { |n| n < 0 } # => false (stops after -2)
|
84
|
+
|
85
|
+
# With validation
|
86
|
+
errors.async.none? { |error| error.critical? }
|
87
|
+
# Checks all errors in parallel, returns false on first critical
|
88
|
+
```
|
89
|
+
|
90
|
+
### Implementation Notes
|
91
|
+
- Uses `Concurrent::AtomicBoolean` to track if any element matches
|
92
|
+
- Essentially the inverse of `any?`
|
93
|
+
- Delegates pattern/no-block cases to wrapped enumerable
|
94
|
+
|
95
|
+
## one?
|
96
|
+
|
97
|
+
Asynchronously checks if exactly one element satisfies the given condition.
|
98
|
+
|
99
|
+
Executes the block for each element in parallel and returns true if exactly one element returns a truthy value. Short-circuits and returns false as soon as a second match is found.
|
100
|
+
|
101
|
+
### Parameters
|
102
|
+
- `pattern` (optional): Pattern to match against elements
|
103
|
+
- `&block`: Block to test each element
|
104
|
+
|
105
|
+
### Returns
|
106
|
+
`Boolean` - true if exactly one element satisfies the condition
|
107
|
+
|
108
|
+
### Examples
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
# Check for single admin
|
112
|
+
users.async.one? { |u| u.admin? } # => true if exactly one admin
|
113
|
+
|
114
|
+
# With validation
|
115
|
+
configs.async.one? { |c| c.primary? }
|
116
|
+
# Validates all configs in parallel, ensures only one is primary
|
117
|
+
```
|
118
|
+
|
119
|
+
### Implementation Notes
|
120
|
+
- Uses `Concurrent::AtomicFixnum` to count matches
|
121
|
+
- Stops barrier execution when count exceeds 1
|
122
|
+
- Delegates pattern/no-block cases to wrapped enumerable
|
123
|
+
|
124
|
+
## find / detect
|
125
|
+
|
126
|
+
Asynchronously finds the first element that satisfies the given condition.
|
127
|
+
|
128
|
+
**Important:** Returns the **fastest completing** match, not necessarily the first element by position in the collection. Due to parallel execution, whichever element completes evaluation first will be returned. If you need the first element by position, use synchronous `find` instead.
|
129
|
+
|
130
|
+
### Parameters
|
131
|
+
- `ifnone` (optional): Proc to call if no element is found
|
132
|
+
- `&block`: Block to test each element
|
133
|
+
|
134
|
+
### Returns
|
135
|
+
The first matching element, or nil/ifnone result if not found
|
136
|
+
|
137
|
+
### Examples
|
138
|
+
|
139
|
+
```ruby
|
140
|
+
# Find any prime number (fastest to compute)
|
141
|
+
numbers.async.find { |n| prime?(n) }
|
142
|
+
|
143
|
+
# With fallback
|
144
|
+
users.async.find(-> { User.new }) { |u| u.admin? }
|
145
|
+
# Returns new User if no admin found
|
146
|
+
|
147
|
+
# With expensive checks - returns fastest result
|
148
|
+
documents.async.find { |doc| analyze_content(doc).contains_keyword? }
|
149
|
+
# Analyzes all documents in parallel, returns fastest match
|
150
|
+
|
151
|
+
# When order matters, use synchronous version
|
152
|
+
first_prime = numbers.find { |n| prime?(n) }
|
153
|
+
```
|
154
|
+
|
155
|
+
### Implementation Notes
|
156
|
+
- Uses `Concurrent::AtomicReference` with compare-and-set for first completion
|
157
|
+
- Returns whichever matching element completes evaluation first
|
158
|
+
- Supports `ifnone` proc for custom fallback behavior
|
159
|
+
- Stops all remaining evaluations once a match is found
|
160
|
+
|
161
|
+
## find_index
|
162
|
+
|
163
|
+
Asynchronously finds the index of the first element that satisfies the given condition.
|
164
|
+
|
165
|
+
**Important:** Returns the index of the **fastest completing** match, not necessarily the first by position in the collection. Due to parallel execution, whichever element completes evaluation first will have its index returned. If you need the first index by position, use synchronous `find_index` instead.
|
166
|
+
|
167
|
+
### Parameters
|
168
|
+
- `value` (optional): Value to find the index of
|
169
|
+
- `&block`: Block to test each element
|
170
|
+
|
171
|
+
### Returns
|
172
|
+
`Integer` or `nil` - Index of first matching element, or nil if not found
|
173
|
+
|
174
|
+
### Examples
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
# Find index of any large file (fastest to check)
|
178
|
+
files.async.find_index { |f| f.size > 1_000_000 }
|
179
|
+
|
180
|
+
# Find specific value - returns index of fastest equality check
|
181
|
+
items.async.find_index("target")
|
182
|
+
|
183
|
+
# With validation - returns index of fastest validation
|
184
|
+
results.async.find_index { |r| r.status == :success }
|
185
|
+
|
186
|
+
# When order matters, use synchronous version
|
187
|
+
first_index = data.find_index { |item| expensive_check(item) }
|
188
|
+
```
|
189
|
+
|
190
|
+
### Implementation Notes
|
191
|
+
- Uses `Concurrent::AtomicReference` with compare-and-set for first completion
|
192
|
+
- Returns index of whichever element completes evaluation first
|
193
|
+
- Handles both value-based and block-based searches
|
194
|
+
- Stops all remaining evaluations once a match is found
|
195
|
+
|
196
|
+
## include? / member?
|
197
|
+
|
198
|
+
Asynchronously checks if the enumerable includes a given value.
|
199
|
+
|
200
|
+
Checks all elements in parallel for equality with the given value. Short-circuits and returns true as soon as a match is found.
|
201
|
+
|
202
|
+
### Parameters
|
203
|
+
- `obj`: Object to search for
|
204
|
+
|
205
|
+
### Returns
|
206
|
+
`Boolean` - true if the enumerable includes the object
|
207
|
+
|
208
|
+
### Examples
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
# Check for value
|
212
|
+
[1, 2, 3].async.include?(2) # => true
|
213
|
+
|
214
|
+
# With objects
|
215
|
+
users.async.include?(target_user)
|
216
|
+
|
217
|
+
# Complex equality
|
218
|
+
configs.async.include?(production_config)
|
219
|
+
```
|
220
|
+
|
221
|
+
### Implementation Notes
|
222
|
+
- Uses `Concurrent::AtomicBoolean` for thread-safe early termination
|
223
|
+
- Relies on the object's `==` method for comparison
|
224
|
+
- Short-circuits on first match
|
225
|
+
|
226
|
+
## Pattern Matching Support
|
227
|
+
|
228
|
+
When called with a pattern argument instead of a block, predicate methods delegate to the wrapped enumerable's synchronous implementation. This ensures correct behavior with Ruby's pattern matching:
|
229
|
+
|
230
|
+
```ruby
|
231
|
+
# Pattern matching
|
232
|
+
[1, 2, 3].async.any?(Integer) # => true
|
233
|
+
[:a, :b].async.all?(Symbol) # => true
|
234
|
+
[1, 2, 3].async.none?(String) # => true
|
235
|
+
[1, 2, 3].async.one?(2) # => true
|
236
|
+
```
|
237
|
+
|
238
|
+
## No-Block Behavior
|
239
|
+
|
240
|
+
When called without a block or pattern, predicate methods check for truthiness:
|
241
|
+
|
242
|
+
```ruby
|
243
|
+
[nil, false, 1].async.any? # => true (1 is truthy)
|
244
|
+
[1, 2, 3].async.all? # => true (all truthy)
|
245
|
+
[nil, false].async.none? # => true (none truthy)
|
246
|
+
[nil, false, 1].async.one? # => true (exactly one truthy)
|
247
|
+
```
|
248
|
+
|
249
|
+
## Performance Considerations
|
250
|
+
|
251
|
+
- All predicate methods use early termination to minimize unnecessary work
|
252
|
+
- Thread-safe atomic variables prevent race conditions
|
253
|
+
- Barrier stops immediately when result is determined
|
254
|
+
- Pattern/no-block cases delegate to avoid async overhead when not needed
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# Transformer Methods
|
2
|
+
|
3
|
+
Transformer methods create modified collections from the original enumerable. All transformer methods return an `Async::Enumerator` for chaining operations.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
Transformer methods delegate to the standard Enumerable implementation but wrap the result in a new `Async::Enumerator` to enable continued chaining. The actual transformation happens through the parent Enumerable module.
|
8
|
+
|
9
|
+
## Available Methods
|
10
|
+
|
11
|
+
### map / collect
|
12
|
+
|
13
|
+
Transforms each element using the given block.
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
[1, 2, 3].async.map { |n| n * 2 } # => Async::Enumerator([2, 4, 6])
|
17
|
+
```
|
18
|
+
|
19
|
+
### select / filter / find_all
|
20
|
+
|
21
|
+
Selects elements for which the block returns true.
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
[1, 2, 3, 4].async.select { |n| n.even? } # => Async::Enumerator([2, 4])
|
25
|
+
```
|
26
|
+
|
27
|
+
### reject
|
28
|
+
|
29
|
+
Rejects elements for which the block returns true.
|
30
|
+
|
31
|
+
```ruby
|
32
|
+
[1, 2, 3, 4].async.reject { |n| n.even? } # => Async::Enumerator([1, 3])
|
33
|
+
```
|
34
|
+
|
35
|
+
### filter_map
|
36
|
+
|
37
|
+
Maps and filters in a single pass, removing nil values.
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
[1, 2, 3, 4].async.filter_map { |n| n * 2 if n.even? } # => Async::Enumerator([4, 8])
|
41
|
+
```
|
42
|
+
|
43
|
+
### flat_map / collect_concat
|
44
|
+
|
45
|
+
Maps and flattens the result by one level.
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
[[1, 2], [3, 4]].async.flat_map { |arr| arr.map { |n| n * 2 } }
|
49
|
+
# => Async::Enumerator([2, 4, 6, 8])
|
50
|
+
```
|
51
|
+
|
52
|
+
### compact
|
53
|
+
|
54
|
+
Removes nil elements.
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
[1, nil, 2, nil, 3].async.compact # => Async::Enumerator([1, 2, 3])
|
58
|
+
```
|
59
|
+
|
60
|
+
### uniq
|
61
|
+
|
62
|
+
Removes duplicate elements.
|
63
|
+
|
64
|
+
```ruby
|
65
|
+
[1, 1, 2, 2, 3].async.uniq # => Async::Enumerator([1, 2, 3])
|
66
|
+
```
|
67
|
+
|
68
|
+
### sort
|
69
|
+
|
70
|
+
Sorts elements using their natural ordering or a provided comparison.
|
71
|
+
|
72
|
+
```ruby
|
73
|
+
[3, 1, 2].async.sort # => Async::Enumerator([1, 2, 3])
|
74
|
+
[3, 1, 2].async.sort { |a, b| b <=> a } # => Async::Enumerator([3, 2, 1])
|
75
|
+
```
|
76
|
+
|
77
|
+
### sort_by
|
78
|
+
|
79
|
+
Sorts elements by the result of the given block.
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
users.async.sort_by { |u| u.age } # Sorts users by age
|
83
|
+
files.async.sort_by { |f| f.size } # Sorts files by size
|
84
|
+
```
|
85
|
+
|
86
|
+
## Chaining
|
87
|
+
|
88
|
+
All transformer methods return an `Async::Enumerator`, enabling method chaining:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
result = [1, 2, 3, 4, 5].async
|
92
|
+
.map { |n| n * 2 } # [2, 4, 6, 8, 10]
|
93
|
+
.select { |n| n > 4 } # [6, 8, 10]
|
94
|
+
.map { |n| n + 1 } # [7, 9, 11]
|
95
|
+
.sort { |a, b| b <=> a } # [11, 9, 7]
|
96
|
+
.sync # Materializes the result
|
97
|
+
```
|
98
|
+
|
99
|
+
## Implementation Notes
|
100
|
+
|
101
|
+
- Transformer methods leverage the standard Enumerable module for transformation logic
|
102
|
+
- Each method wraps the result in a new `Async::Enumerator` with the same fiber limit
|
103
|
+
- Methods returning enumerators without blocks are handled correctly
|
104
|
+
- The `max_fibers` setting is preserved through the transformation chain
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Async
|
2
|
+
module Enumerable
|
3
|
+
module Comparable
|
4
|
+
def self.included(base)
|
5
|
+
base.include(::Comparable)
|
6
|
+
end
|
7
|
+
|
8
|
+
# Compares with another enumerable.
|
9
|
+
# @param other [Object] Object to compare
|
10
|
+
# @return [Integer, nil] Comparison result
|
11
|
+
def <=>(other)
|
12
|
+
return nil unless other.respond_to?(:to_a)
|
13
|
+
to_a <=> other.to_a
|
14
|
+
end
|
15
|
+
|
16
|
+
# Checks equality with another enumerable.
|
17
|
+
# @param other [Object] Object to compare
|
18
|
+
# @return [Boolean] True if equal
|
19
|
+
def ==(other)
|
20
|
+
return false unless other.respond_to?(:to_a)
|
21
|
+
to_a == other.to_a
|
22
|
+
end
|
23
|
+
alias_method :eql?, :==
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Async
|
4
|
+
module Enumerable
|
5
|
+
# Provides bounded concurrency control for async operations.
|
6
|
+
# See docs/reference/concurrency_bounder.md for detailed documentation.
|
7
|
+
# @api private
|
8
|
+
module ConcurrencyBounder
|
9
|
+
def self.included(base) = base.include(Configurable)
|
10
|
+
|
11
|
+
# Executes block with bounded concurrency.
|
12
|
+
# @param early_termination [Boolean] Support early stop
|
13
|
+
# @yield [barrier] Barrier for spawning async tasks
|
14
|
+
def __async_enumerable_bounded_concurrency(early_termination: false, limit: nil, &block)
|
15
|
+
Sync do |parent|
|
16
|
+
limit ||= __async_enumerable_config.max_fibers
|
17
|
+
semaphore = Async::Semaphore.new(limit, parent:)
|
18
|
+
barrier = Async::Barrier.new(parent: semaphore)
|
19
|
+
|
20
|
+
# Yield the barrier for task spawning
|
21
|
+
yield barrier
|
22
|
+
|
23
|
+
# Wait for all tasks to complete (or early termination)
|
24
|
+
if early_termination
|
25
|
+
begin
|
26
|
+
barrier.wait
|
27
|
+
rescue Async::Stop
|
28
|
+
# Expected when barrier.stop is called for early termination
|
29
|
+
end
|
30
|
+
else
|
31
|
+
barrier.wait
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,140 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Async
|
4
|
+
module Enumerable
|
5
|
+
# Manages configuration and collection resolution for async enumerables.
|
6
|
+
# Provides a DSL for defining async enumerable sources and configuration.
|
7
|
+
module Configurable
|
8
|
+
# Configuration data class for managing async enumerable settings.
|
9
|
+
# Uses Ruby's Data class for immutability and clean API.
|
10
|
+
class Config < Data.define(:collection_ref, :max_fibers)
|
11
|
+
DEFAULT_MAX_FIBERS = 1024
|
12
|
+
|
13
|
+
def initialize(collection_ref: nil, max_fibers: DEFAULT_MAX_FIBERS)
|
14
|
+
super
|
15
|
+
end
|
16
|
+
|
17
|
+
# Define the struct class once to avoid redefinition warnings
|
18
|
+
ConfigStruct = Struct.new(*members)
|
19
|
+
|
20
|
+
# Creates mutable struct for configuration editing.
|
21
|
+
# @return [ConfigStruct] Mutable config struct
|
22
|
+
def to_struct
|
23
|
+
ConfigStruct.new(*deconstruct)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class << self
|
28
|
+
def included(base)
|
29
|
+
unless base.instance_variable_get(:@__async_enumerable_config_ref)
|
30
|
+
ref = Concurrent::AtomicReference.new
|
31
|
+
base.instance_variable_set(:@__async_enumerable_config_ref, ref)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Class methods for defining async enumerable sources
|
37
|
+
module ClassMethods
|
38
|
+
# Defines enumerable source for async operations.
|
39
|
+
# @param collection_ref [Symbol] Method/ivar returning enumerable
|
40
|
+
# @param kwargs [Hash] Configuration options (max_fibers, etc.)
|
41
|
+
def def_async_enumerable(collection_ref = nil, **kwargs)
|
42
|
+
# Store only the class-specific overrides, not a full config
|
43
|
+
@__async_enumerable_class_overrides = {collection_ref:}.compact.merge(kwargs)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Gets the collection reference from config.
|
47
|
+
# @return [Symbol, nil] Collection reference
|
48
|
+
def __async_enumerable_collection_ref
|
49
|
+
__async_enumerable_config.collection_ref
|
50
|
+
end
|
51
|
+
|
52
|
+
# Gets config with class-level overrides merged.
|
53
|
+
# @return [Config] Merged configuration
|
54
|
+
def __async_enumerable_config
|
55
|
+
# Dynamically merge module config with class overrides
|
56
|
+
base = Async::Enumerable.config
|
57
|
+
if @__async_enumerable_class_overrides
|
58
|
+
base.with(**@__async_enumerable_class_overrides)
|
59
|
+
else
|
60
|
+
base
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Returns nil as classes don't cache config refs.
|
65
|
+
# @return [nil] Always nil for class level
|
66
|
+
def __async_enumerable_config_ref
|
67
|
+
nil
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
# Instance methods for configuration and collection resolution
|
72
|
+
|
73
|
+
# Gets or updates configuration with block.
|
74
|
+
# @yield [ConfigStruct] Mutable config for editing
|
75
|
+
# @return [Config] Current or updated configuration
|
76
|
+
def __async_enumerable_configure
|
77
|
+
# Get the current config (with hierarchy)
|
78
|
+
if @__async_enumerable_config_ref
|
79
|
+
current = @__async_enumerable_config_ref.get
|
80
|
+
else
|
81
|
+
# Build config from hierarchy
|
82
|
+
current_hash = __async_enumerable_merge_all_config
|
83
|
+
current = Config.new(**current_hash)
|
84
|
+
end
|
85
|
+
|
86
|
+
return current unless block_given?
|
87
|
+
|
88
|
+
mutable = current.to_struct
|
89
|
+
yield mutable
|
90
|
+
final = __async_enumerable_merge_all_config(mutable.to_h)
|
91
|
+
|
92
|
+
Config.new(**final).tap do |updated|
|
93
|
+
@__async_enumerable_config_ref = Concurrent::AtomicReference.new(updated)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
alias_method :__async_enumerable_config, :__async_enumerable_configure
|
97
|
+
|
98
|
+
# Merges configs from all hierarchy levels.
|
99
|
+
# @param config [Hash, nil] Additional config to merge
|
100
|
+
# @return [Hash] Merged configuration hash
|
101
|
+
def __async_enumerable_merge_all_config(config = nil)
|
102
|
+
[Async::Enumerable.config].tap do |arr|
|
103
|
+
class_cfg = self.class.respond_to?(:__async_enumerable_config) ? self.class.__async_enumerable_config : nil
|
104
|
+
|
105
|
+
arr << class_cfg
|
106
|
+
arr << @__async_enumerable_config
|
107
|
+
arr << config
|
108
|
+
end.compact.map(&:to_h).reduce(&:merge)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Gets the config reference for this object.
|
112
|
+
# @return [AtomicReference] Config reference
|
113
|
+
def __async_enumerable_config_ref
|
114
|
+
# First check for instance-level config ref
|
115
|
+
@__async_enumerable_config_ref || Async::Enumerable.config_ref
|
116
|
+
end
|
117
|
+
|
118
|
+
# Collection resolution methods
|
119
|
+
|
120
|
+
# Gets collection reference from class.
|
121
|
+
# @return [Symbol, nil] Collection reference
|
122
|
+
def __async_enumerable_collection_ref
|
123
|
+
self.class.__async_enumerable_collection_ref
|
124
|
+
end
|
125
|
+
|
126
|
+
# Resolves the actual enumerable collection.
|
127
|
+
# @return [Enumerable] The collection to enumerate
|
128
|
+
def __async_enumerable_collection
|
129
|
+
return self unless __async_enumerable_collection_ref.is_a?(Symbol)
|
130
|
+
|
131
|
+
ref = __async_enumerable_collection_ref
|
132
|
+
if ref.to_s.start_with?("@")
|
133
|
+
instance_variable_get(ref)
|
134
|
+
else
|
135
|
+
send(ref)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Async
|
4
|
+
module Enumerable
|
5
|
+
module Methods
|
6
|
+
# Aggregators module for enumerable aggregation methods.
|
7
|
+
#
|
8
|
+
# Aggregation methods like reduce, inject, sum, count, and tally are
|
9
|
+
# inherited from the standard Enumerable module. When used with async
|
10
|
+
# enumerables, these methods automatically benefit from parallel execution
|
11
|
+
# through our async #each implementation.
|
12
|
+
#
|
13
|
+
# The block passed to these methods (when applicable) executes concurrently
|
14
|
+
# for each element, though the aggregation itself maintains correct ordering
|
15
|
+
# and thread-safe accumulation.
|
16
|
+
#
|
17
|
+
# Methods available through Enumerable:
|
18
|
+
# - reduce/inject: Combines elements using a binary operation
|
19
|
+
# - sum: Calculates the sum of elements (block executes async)
|
20
|
+
# - count: Counts elements matching a condition (block executes async)
|
21
|
+
# - tally: Counts occurrences of each element
|
22
|
+
# - min/max/minmax: Finds minimum/maximum elements (block executes async)
|
23
|
+
# - min_by/max_by: Finds elements by computed values (block executes async)
|
24
|
+
module Aggregators
|
25
|
+
def self.included(base)
|
26
|
+
base.include(Each) # Dependency
|
27
|
+
base.include(Configurable) # Dependency for collection resolution
|
28
|
+
|
29
|
+
# Delegate non-parallelizable aggregator methods directly to the collection
|
30
|
+
base.extend(Forwardable)
|
31
|
+
# is lazy really an aggregator? no, but i don't want to figure out
|
32
|
+
# _what_ it is either
|
33
|
+
base.def_delegators :__async_enumerable_collection, :size, :length, :lazy
|
34
|
+
end
|
35
|
+
# This module is intentionally empty as aggregation methods are
|
36
|
+
# inherited from Enumerable and automatically use our async #each
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|