-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Implement ruby-style Enumerator #6385
Draft
felixbuenemann
wants to merge
1
commit into
crystal-lang:master
Choose a base branch
from
felixbuenemann:enumerator
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
require "spec" | ||
require "enumerator" | ||
|
||
describe Enumerator do | ||
describe "initialize" do | ||
it "returns an instance of Enumerator" do | ||
instance = Enumerator(String).new { } | ||
instance.should be_a(Enumerator(String)) | ||
end | ||
end | ||
|
||
describe "yielder" do | ||
it "accepts values of enumerator type" do | ||
enumerator = Enumerator(String).new do |yielder| | ||
yielder << "hello" | ||
yielder << "world" | ||
end | ||
|
||
enumerator.next.should eq("hello") | ||
enumerator.next.should eq("world") | ||
end | ||
|
||
it "allows to chain yielder calls" do | ||
enumerator = Enumerator(String).new do |yielder| | ||
yielder << "hello" << "world" | ||
end | ||
|
||
enumerator.next.should eq("hello") | ||
enumerator.next.should eq("world") | ||
end | ||
end | ||
|
||
describe "next" do | ||
it "returns the values in the order they were yielded" do | ||
enumerator = Enumerator(String?).new do |yielder| | ||
yielder << "hello" | ||
yielder << nil | ||
yielder << "world" | ||
end | ||
|
||
enumerator.next.should eq("hello") | ||
enumerator.next.should be_nil | ||
enumerator.next.should eq("world") | ||
enumerator.next.should be_a(Iterator::Stop) | ||
end | ||
|
||
it "works when the type is Nil" do | ||
enumerator = Enumerator(Nil).new do |yielder| | ||
yielder << nil | ||
yielder << nil | ||
end | ||
|
||
enumerator.next.should be_nil | ||
enumerator.next.should be_nil | ||
enumerator.next.should be_a(Iterator::Stop) | ||
end | ||
|
||
it "starts the yielder block at the first call" do | ||
started = false | ||
enumerator = Enumerator(Int32).new do |y| | ||
started = true | ||
y << 1 | ||
end | ||
|
||
started.should be_false | ||
enumerator.next.should eq(1) | ||
started.should be_true | ||
end | ||
|
||
it "cooperates with other fibers outside the enumerator" do | ||
enumerator = Enumerator(Int32).new do |y| | ||
y << 1 | ||
end | ||
|
||
value = 0 | ||
spawn do | ||
value = enumerator.next | ||
end | ||
|
||
Fiber.yield | ||
|
||
value.should eq(1) | ||
end | ||
|
||
it "cooperates with other fibers inside the enumerator" do | ||
enumerator = Enumerator(Int32).new do |y| | ||
spawn do | ||
y << 1 | ||
end | ||
Fiber.yield | ||
y << 2 | ||
end | ||
|
||
enumerator.next.should eq(1) | ||
enumerator.next.should eq(2) | ||
end | ||
end | ||
|
||
describe "peek" do | ||
it "peeks at the next value without affecting next" do | ||
enumerator = Enumerator(String).new do |yielder| | ||
yielder << "hello" | ||
yielder << "world" | ||
end | ||
|
||
enumerator.peek.should eq("hello") | ||
enumerator.peek.should eq("hello") | ||
enumerator.next.should eq("hello") | ||
enumerator.peek.should eq("world") | ||
enumerator.peek.should eq("world") | ||
enumerator.next.should eq("world") | ||
enumerator.peek.should be_a(Iterator::Stop) | ||
enumerator.next.should be_a(Iterator::Stop) | ||
end | ||
|
||
it "does not affect rewind" do | ||
enumerator = Enumerator(String).new do |yielder| | ||
yielder << "hello" | ||
yielder << "world" | ||
end | ||
|
||
enumerator.next.should eq("hello") | ||
enumerator.peek.should eq("world") | ||
enumerator.rewind | ||
enumerator.peek.should eq("hello") | ||
enumerator.next.should eq("hello") | ||
end | ||
|
||
it "works when the type is Nil" do | ||
enumerator = Enumerator(Nil).new do |yielder| | ||
yielder << nil | ||
yielder << nil | ||
end | ||
|
||
enumerator.peek.should be_nil | ||
enumerator.next.should be_nil | ||
enumerator.peek.should be_nil | ||
enumerator.next.should be_nil | ||
enumerator.peek.should be_a(Iterator::Stop) | ||
enumerator.next.should be_a(Iterator::Stop) | ||
end | ||
end | ||
|
||
describe "each" do | ||
it "iterates the yielded values" do | ||
enumerator = Enumerator(String).new do |yielder| | ||
yielder << "hello" | ||
yielder << "world" | ||
end | ||
|
||
values = [] of String | ||
enumerator.each { |value| values << value } | ||
values.should eq(["hello", "world"]) | ||
end | ||
end | ||
|
||
describe "rewind" do | ||
it "rewinds the iterator after full iteration" do | ||
enumerator = Enumerator(String).new do |yielder| | ||
yielder << "hello" | ||
yielder << "world" | ||
end | ||
|
||
enumerator.next.should eq("hello") | ||
enumerator.next.should eq("world") | ||
enumerator.next.should be_a(Iterator::Stop) | ||
|
||
enumerator.rewind.should be_a(Enumerator(String)) | ||
|
||
enumerator.next.should eq("hello") | ||
end | ||
|
||
it "rewinds the iterator after partial iteration" do | ||
enumerator = Enumerator(String).new do |yielder| | ||
yielder << "hello" | ||
yielder << "world" | ||
end | ||
enumerator.next.should eq("hello") | ||
|
||
enumerator.rewind.should be_a(Enumerator(String)) | ||
enumerator.next.should eq("hello") | ||
end | ||
|
||
it "can be called before the first iteration" do | ||
enumerator = Enumerator(String).new do |yielder| | ||
yielder << "hello" | ||
yielder << "world" | ||
end | ||
|
||
enumerator.rewind.should be_a(Enumerator(String)) | ||
|
||
enumerator.next.should eq("hello") | ||
enumerator.next.should eq("world") | ||
enumerator.next.should be_a(Iterator::Stop) | ||
end | ||
|
||
it "can be rewound multiple times" do | ||
enumerator = Enumerator(String).new do |yielder| | ||
yielder << "hello" | ||
yielder << "world" | ||
end | ||
|
||
enumerator.next.should eq("hello") | ||
enumerator.next.should eq("world") | ||
enumerator.next.should be_a(Iterator::Stop) | ||
|
||
enumerator.rewind.should be_a(Enumerator(String)) | ||
enumerator.rewind.should be_a(Enumerator(String)) | ||
|
||
enumerator.next.should eq("hello") | ||
end | ||
end | ||
|
||
describe "Fibonacci enumerator" do | ||
it "generates the first 10 numbers in the Fibonacci sequence" do | ||
# Make sure example from the Enumerator docs works. | ||
fib = Enumerator(Int32).new do |y| | ||
a = b = 1 | ||
loop do | ||
y << a | ||
a, b = b, a + b | ||
end | ||
end | ||
|
||
fib.first(10).to_a.should eq([1, 1, 2, 3, 5, 8, 13, 21, 34, 55]) | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
require "./weak_ref" | ||
|
||
# An `Enumerator` allows to lazily yield values from a block and consume them as `Iterator`. | ||
# | ||
# It is useful, if you have a block that generates values and needs to block in the middle | ||
# of execution until the next value is consumed. | ||
# | ||
# You should use an `Iterator` instead, if you do not need this blocking behavior, since it | ||
# is much more efficient. | ||
# | ||
# As an example, let's build a generator that yields numbers from the Fibonacci Sequence: | ||
# | ||
# ``` | ||
# fib = Enumerator(Int32).new do |y| | ||
# a = b = 1 | ||
# loop do | ||
# y << a | ||
# a, b = b, a + b | ||
# end | ||
# end | ||
# | ||
# fib.first(10).to_a # => [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] | ||
# ``` | ||
# | ||
# Note that the above example works, but it would be much faster using an `Iterator`: | ||
# | ||
# ``` | ||
# a = b = 1 | ||
# fib = Iterator.of do | ||
# a.tap { a, b = b, a + b } | ||
# end | ||
# | ||
# fib.first(10).to_a # => [1, 1, 2, 3, 5, 8, 13, 21, 34, 55] | ||
# ``` | ||
# It is generally advisable to avoid `Enumerator` in cases where you have a large number | ||
# of iterations and the computation inside the block is trivial, because the additional | ||
# overhead of the blocking behavior that is implemented with `Fiber` under the hood will | ||
# negatively impact performance. | ||
# | ||
# However for more complex examples you will find that it might not be possible to replace | ||
# the loop with suspendable state and you would have to build your own blocking behavior | ||
# using for example an `Iterator` and a `Channel`. | ||
# | ||
# Using `Enumerator` is the right fit for those cases and it handles a lot of the tricky | ||
# edge cases like ensuring the block is not started until you consume the first value or | ||
# aborted if you rewind or abandon the iterator. | ||
class Enumerator(T) | ||
include Iterator(T) | ||
|
||
# :nodoc: | ||
getter! current_fiber | ||
|
||
@fiber : Fiber? | ||
@done : Bool? | ||
|
||
def initialize(&@block : Yielder(T) ->) | ||
@current_fiber = Fiber.current | ||
@stack = Deque(T).new(1) | ||
@yielder = Yielder(T).new(self) | ||
run | ||
end | ||
|
||
# Returns the next element yielded from the block or `Iterator::Stop::INSTANCE` | ||
# if there are no more elements. | ||
def next | ||
fetch_next | ||
stack.shift { stop } | ||
end | ||
|
||
# Rewinds the iterator and restarts the yielder block. | ||
def rewind | ||
fiber.kill | ||
run | ||
self | ||
end | ||
|
||
# Peeks at the next value without forwarding the iterator. | ||
def peek | ||
fetch_next | ||
if stack.empty? | ||
stop | ||
else | ||
stack[0] | ||
end | ||
end | ||
|
||
# :nodoc: | ||
def finalize | ||
fiber.kill | ||
end | ||
|
||
protected def <<(value : T) | ||
stack.push(value) | ||
end | ||
|
||
private getter stack | ||
private getter! fiber | ||
private getter? done | ||
|
||
private def fetch_next : Nil | ||
@current_fiber = Fiber.current | ||
fiber.resume if fiber.alive? && stack.empty? | ||
end | ||
|
||
private def run | ||
stack.clear | ||
|
||
block = @block | ||
yielder = @yielder.not_nil! | ||
|
||
@fiber = Fiber.new do | ||
block.call(yielder) | ||
end.on_finish do | ||
yielder.resume_current_fiber | ||
end | ||
end | ||
|
||
private struct Yielder(T) | ||
@enumerator : WeakRef(Enumerator(T)) | ||
|
||
def initialize(enumerator : Enumerator(T)) | ||
@enumerator = WeakRef.new(enumerator) | ||
end | ||
|
||
# Yields the next value to the enumerator. | ||
def <<(value : T) | ||
pass_to_enumerator(value) | ||
resume_current_fiber | ||
self | ||
end | ||
|
||
# :nodoc: | ||
def resume_current_fiber | ||
current_fiber = get_current_fiber | ||
current_fiber.try &.resume | ||
end | ||
|
||
private def pass_to_enumerator(value : T) | ||
@enumerator.value.try &.<< value | ||
end | ||
|
||
private def get_current_fiber | ||
@enumerator.value.try &.current_fiber | ||
end | ||
end | ||
end |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since this is an Enumerator should the return type be an
Enumerator::Stop
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't tested it, but since the
Iterator
module is included it probably already works with bothEnumerator::Stop
andIterator::Stop
.