Раздел «Язык Ruby».RubyCourseLecture16:
<<Метапрограммирование на 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