Вступление в мета-программирование на Ruby: создание прокси-методов для нескольких внутренних методов - PullRequest
1 голос
/ 14 июня 2010

Я много раз слышал, как Руби рекламируют свои супер-впечатляющие возможности метапрограммирования, и мне было интересно, кто-нибудь может мне помочь с этой проблемой.

У меня есть класс, который работает как своего рода «архив», с внутренними методами, которые обрабатывают и выводят данные на основе входных данных. Однако элементы в архиве в самом классе представлены и обработаны целыми числами для повышения производительности. Фактические элементы вне архива известны их строковым представлением, которое просто равно number_representation.to_s (36).

Из-за этого я подключил каждый внутренний метод к «прокси-методу», который преобразует входные данные в целочисленную форму, которую распознает архив, запускает внутренний метод и преобразует выходные данные (либо один другой элемент, либо их коллекция) обратно в строки.

Соглашение об именах таково: внутренние методы представлены как _method_name; соответствующий им прокси-метод представлен method_name без начального подчеркивания.

Например:

class Archive

  ## PROXY METHODS ##
  ## input: string representation of id's
  ## output: string representation of id's

  def do_something_with id
    result = _do_something_with id.to_i(36)
    return nil if result == nil
    return result.to_s(36)
  end

  def do_something_with_pair id_1,id_2
    result = _do_something_with_pair id_1.to_i(36), id_2.to_i(36)
    return nil if result == nil
    return result.to_s(36)
  end

  def do_something_with_these ids
    result = _do_something_with_these ids.map { |n| n.to_i(36) }
    return nil if result == nil
    return result.to_s(36)
  end

  def get_many_from id
    result = _get_many_from id
    return nil if result == nil         # no sparse arrays returned
    return result.map { |n| n.to_s(36) }
  end

  ## INTERNAL METHODS ##
  ## input: integer representation of id's
  ## output: integer representation of id's

  private

  def _do_something_with id
    # does something with one integer-represented id,
    # returning an id represented as an integer
  end

  def do_something_with_pair id_1,id_2
    # does something with two integer-represented id's,
    # returning an id represented as an integer
  end

  def _do_something_with_these ids
    # does something with multiple integer ids,
    # returning an id represented as an integer
  end

  def _get_many_from id
    # does something with one integer-represented id,
    # returns a collection of id's represented as integers
  end
end

Есть несколько причин, по которым я не могу просто преобразовать их, если id.class == Строка в начале внутренних методов:

  1. Эти внутренние методы являются в некоторой степени вычислительно-рекурсивными функциями, и я не хочу, чтобы накладные расходы проверялись несколько раз на каждом шаге
  2. Невозможно без добавления дополнительного параметра указать, следует ли преобразовывать в конце
  3. Я хочу воспринимать это как упражнение в понимании метапрограммирования в ruby ​​

У кого-нибудь есть идеи?


редактировать

Я бы предпочел решение, которое могло бы принимать массив имен методов

@@PROXY_METHODS = [:do_something_with, :do_something_with_pair,
                   :do_something_with_these, :get_many_from]

итерируйте их, и на каждой итерации выведите прокси-метод. Я не уверен, что будет сделано с аргументами, но есть ли способ проверить аргументы метода? Если нет, то подойдет и простая утка / аналогичная концепция.


Я придумал собственное решение, используя #class_eval

@@PROXY_METHODS.each do |proxy|
  class_eval %{ def #{proxy} *args
                  args.map! do |a|
                    if a.class == String
                      a.to_i(36)
                    else
                      a.map { |id| id.to_i(36) }
                    end
                  end
                  result = _#{proxy}(*args)

                  result and if result.respond_to?(:each)
                               result.map { |r| r.to_s(36) }
                             else
                               result.to_s(36)
                             end
                end
              }
end

Однако #class_eval кажется немного ... грязным? или не элегантный по сравнению с тем, что "должно" быть.

1 Ответ

2 голосов
/ 14 июня 2010
class Archive
  # define a new method-creating method for Archive by opening the
  # singleton class for Archive
  class << Archive
    private # (make it private so no one can call Archive.def_api_method)
    def def_api_method name, &defn
      define_method(name) do |*args|
        # map the arguments to their integer equivalents,
        # and pass them to the method definition
        res = defn[ *args.map { |a| a.to_i(36) } ]
        # if we got back a non-nil response, 
        res and if res.respond_to?(:each)
                  # map all of the results if many returned
                  res.map { |r| r.to_s(36) } 
                else
                  # map the only result if only one returned
                  res.to_s(36)
                end
      end
    end
  end
  def_api_method("do_something_with"){ |id| _do_something_with(id) }
  def_api_method("do_something_with_pair"){ |id_1, id_2| _do_something_with_pair id_1.to_i(36), id_2.to_i(36) }
  #...
end

Вместо того, чтобы открывать синглтон для определения Archive.def_api_method, вы можете определить его просто, используя

class Archive
  def Archive.def_api_method
    #...

Но причина, по которой я этого не сделал, это то, что кто-то имеет доступ к Archive класс может вызвать его, используя Archive.def_api_method.Открытие синглтон-класса позволило мне пометить def_api_method как приватное, поэтому его можно вызывать только тогда, когда self == Archive.

Если вы всегда будете вызывать внутреннюю версию с той же (или производной)), тогда вы можете просто вызвать его напрямую (а не передать блок определения), используя #send.

class Archive
  # define a method-creating method that wraps an internal method for external use
  class << Archive
    private # (make it private so no one can call Archive.api_method)
    def api_method private_name
      public_name = private_name.to_s.sub(/^_/,'').to_sym
      define_method(public_name) do |*args|
        # map the arguments to their integer equivalents,
        # and pass them to the private method
        res = self.send(private_name, *args.map { |a| a.to_i(36) })
        # if we got back a non-nil response, 
        res and if res.respond_to?(:each)
                  # map all of the results if many returned
                  res.map { |r| r.to_s(36) } 
                else
                  # map the only result if only one returned
                  res.to_s(36)
                end          end
      # make sure the public method is publicly available
      public public_name
    end
  end

  api_method :_do_something_with
  api_method :_do_something_with_pair

  private

  def _do_something_with
    #...
  end
  def _do_something_with_pair
    #...
  end
end

Это больше похоже на то, что делается другими мета-методами, такими как attr_reader и attr_writer.

...