I've been given an interesting ruby question for a technical test. I did not find an answer in the time given, but i think it was interesting and would like someone else's take on this. How would you write code to do:
2.times.each_second { p 'works' }
I tried extending the enumerator class, with a per_second method. The per_second method just sleeps for the correct number of milliseconds. But i was stuck trying to pass the "2" argument to the per_second method in an elegant fashion. Maybe using the length of the enumerator returned from 2.times?
CodePudding user response:
Using the magic of modern Ruby, we can write a refinement to Integer
that augments the times
method, but only in scopes that opt into this refinement behavior.
module IntegerExt
# Define a refinement of the Integer type. This is like
# monkeypatching but is local to modules or files that opt in to the
# change.
refine Integer do
# Re-write the times function.
def times(&block)
if block
# If a block was provided, then do the "normal" thing.
super(&block)
else
# Otherwise, get an enumerator and augment its singleton class
# with PerSecondEnumerator.
super().extend PerSecondEnumerator
end
end
end
# This module provides the per_second method on enumerators that
# need it.
module PerSecondEnumerator
def per_second(&block)
loop do
sleep(1.0/self.size)
block.call
end
end
end
end
# If a module wishes to have this functionality, it needs to opt in
# with this 'using' statement. The per_second syntax only works on
# objects constructed within the lexical scope of this refinement.
using IntegerExt
puts 2.times.inspect # Still works
2.times.per_second { p 'works' }
This deals with a couple of the concerns of Aria's answer. We're no longer globally monkey-patching a built-in class. We only modify the class in the lexical scope of any module that wishes to see our changes. Further, since we've refined times
rather than Enumerator
directly, our per_second
only works on enumerators constructed via times
, so it's impossible to do something nonsensical like [1, 2, 3, 4, 5].per_second { ... }
.
CodePudding user response:
You could do it like that:
Enumerator.define_method(:per_second) do |&block|
loop do
sleep(1.0/self.size)
block.call
end
end
3.times.per_second { puts "works" }
But here are some warnings:
It's not recommended to expand a Ruby base class like Enumerator, Integer, etc.
Like someone said in a comment on your post, it's not good to use size, since you can use a string or array instead of a integer.