Skip to content

Instantly share code, notes, and snippets.

@pbrisbin
Created June 12, 2017 22:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pbrisbin/fec60a25dd6c07bbeeaf7bd8119d3afd to your computer and use it in GitHub Desktop.
Save pbrisbin/fec60a25dd6c07bbeeaf7bd8119d3afd to your computer and use it in GitHub Desktop.
#!/usr/bin/env ruby
class Yielder
attr_reader :element
def initialize
# #yield will block until some other thread calls #next
@blocked = true
# #next will block until some other thread calls #yield
@yielded = false
# this is how we know when to raise StopIteration instead of waiting forever
# for someone to do something. this requires that the first #yield is always
# called before the first #next, which we force in our test here but is
# clearly not a guarantee you could ever have in real life.
@yield_waiting = false
end
def yield(x)
# confirm the block did in fact try to yield something
@yield_waiting = true
# wait until something asks us for what we yielded, go back to the other
# thread(s) for as little time as possible, ideally only one frame.
true while @blocked
# immediately re-block
@blocked = true
# set the element that was yielded
@element = x
# adjust our state, freeing up #next
@yielded = true
@yield_waiting = false
end
def next
# we asked for something but nothing has been yielded yet. this is a huge
# race that falls apart in practice without a well-placed sleep...
raise StopIteration unless @yield_waiting
# go ahead, yield me!
@blocked = false
# same idea, wait as little time as possible...
true until @yielded
# adjust our state, freeing up #yield
@blocked = true
@yielded = false
# finally, give up dat element
@element
end
end
class Fnumerator
def initialize(&block)
@yielder = Yielder.new
Thread.new { block.call(@yielder) }
sleep 1 # lulz
end
def next
@yielder.next
end
def take(n)
memo = []
n.times.each do |x|
memo << self.next
end
memo
rescue StopIteration
memo
end
end
# Yields more than 10 times, should give the taken number of elements without
# going into and infinate loop.
x = Fnumerator.new do |yielder|
loop do
yielder.yield(rand(10))
end
end
p x.take(5)
# Yields fewer than 10 times, should give that many elements without waiting
# forever for the next yield.
y = Fnumerator.new do |yielder|
3.times do
yielder.yield(rand(10))
end
end
p y.take(5)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment