<<Метапрограммирование на Ruby
Лекция 16. Make shared iterator (part 2)
Implementation
Приведённая ниже реализация примеси MakeSharedIterator
умеет превращать несколько однопроходных алгоритмов в один однопроходный.
При этом патчи методов действуют только в течение того времени,
когда они необходимы, после этого переопределённые методы
восстанавливаются.
Хэш @@iter_sharing_patched_methods хранит "счётчик ссылок" для
каждого пропатченного метода. Когда этот счётчик ссылок
обращается в нуль, соответствующий метод восстанавливается.
Также в примеси используется переопределение классового метода "included",
который вызывается при примешивании модуля вызовом include MakeSharedIterator.
Строчка "require 'fiber'" необходима для использования "Fiber.current".
require 'fiber'
require 'lib/meta_ext'
module MakeSharedIterator
def self.included(klass)
klass.class_eval do
extend ClassMethods
end
end
module ClassMethods
def make_shared_iterator(new_method,options)
raise if not options[:for] or not options[:methods]
(self.is_a?(Module) ? self : self.singleton_class).class_eval do
@@iter_sharing_fibers ||= {}
@@iter_sharing_patched_methods ||= Hash.new(0)
@@iter_sharing_lock ||= Mutex.new
define_method("#{options[:for]}_patched") do |*args,&block|
if @@iter_sharing_fibers.has_key?(Fiber.current.object_id)
loop do
block[Fiber.yield]
end
else
send "#{options[:for]}_orig", *args, &block
end
end
define_method(new_method) do
@@iter_sharing_lock.synchronize do
if @@iter_sharing_patched_methods[options[:for]] == 0
self.class.send :alias_method, "#{options[:for]}_orig", options[:for]
self.class.send :alias_method, options[:for], "#{options[:for]}_patched"
end
@@iter_sharing_patched_methods[options[:for]] += 1
end
fibers = options[:methods].map {|m| Fiber.new { send m } }
fibers.each {|f| @@iter_sharing_fibers[f.object_id] = true }
fibers.each(&:resume)
send "#{options[:for]}_orig".to_sym do |n|
fibers.each {|f| f.resume(n) }
end
fibers.each {|f| @@iter_sharing_fibers.delete(f.object_id) }
@@iter_sharing_lock.synchronize do
if (@@iter_sharing_patched_methods[options[:for]] -= 1) == 0
self.class.send :alias_method, options[:for], "#{options[:for]}_orig"
end
end
end
end
end
end
end
Demo
Пример использования примеси MakeSharedIterator. Даже после объединения методов squared и powers, каждый из них по-прежнему можно вызывать. Это было достигнуто проверкой идентификатора текущего fiber'а (Fiber.current.object_id).
require 'lib/make_shared_iter'
class My
def each_number
(1..5).each do |n|
puts "number generated (#{n})"
yield n
end
end
def squares
each_number do |n|
puts n**2
end
end
def powers
each_number do |n|
puts 2**n
end
end
def cubes
each_number do |n|
puts n**3
end
end
#===============================
include MakeSharedIterator
make_shared_iterator :do_it,
:methods => [:squares, :powers],
:for => :each_number
make_shared_iterator :do_powers,
:methods => [:squares, :cubes],
:for => :each_number
end
m = My.new
m.do_it
m.squares
m.do_it
m.do_powers
n = My.new
n.do_it
Tests
Для тестирования используется библиотека Test::Unit. Если от класса Test::Unit::TestCase наследовать новый класс (TestSimple), то все методы в нем, названия которых начинаются на "test_", будут считаться тестовыми методами и будут запущены при запуске программы.
Как видно из тестов, объединять можно и больше двух алгоритмов (в данном случае их 3).
$:.unshift(File.dirname(__FILE__))
$:.unshift(File.join(File.dirname(__FILE__), ".."))
$:.unshift(File.join(File.dirname(__FILE__), "..", 'lib'))
require 'make_shared_iter'
require 'test/unit'
class TestSimple < Test::Unit::TestCase
class A
include MakeSharedIterator
class <<self
attr_accessor :ary
end
def each_object(&block)
(1..3).each(&block)
end
def a
each_object do |i|
self.class.ary << 0
end
end
def b
each_object do |i|
self.class.ary << i
end
end
def c
each_object do |i|
self.class.ary << i*i
end
end
make_shared_iterator :do_ab,
:methods => [:a, :b],
:for => :each_object
make_shared_iterator :do_abc,
:methods => [:a, :b, :c],
:for => :each_object
end
#def initialize(method)
# A.ary = []
# method
#end
def test_ab
A.ary = []
A.new.do_ab
#p A.ary
assert_equal([0,1,0,2,0,3], A.ary)
end
def test_abc
A.ary = []
A.new.do_abc
#p A.ary
assert_equal([0,1,1,0,2,4,0,3,9], A.ary)
end
end
--
ArtemVoroztsov - 27 Apr 2010