2014-10-24 5 views
8

Struct позволяет мне создать новый класс, который принимает аргументы и имеет хорошую семантику. Однако аргументы не являются обязательными, и их порядок требует согласования с определением:Метапрограмма, определяющая методы Ruby, которые принимают аргументы ключевых слов?

Point = Struct.new(:x, :y) 

Point.new(111, 222) 
#=> <point instance with x = 111, y = 222> 

Point.new(111) 
#=> <point instance with x = 111, y = nil> 

я хотел бы что-то похожее на структуру, но использует именованные аргументы вместо:

Point = StricterStruct.new(:x, :y) 

Point.new(x: 111, y: 222) 
#=> <point instance with x = 111, y = 222> 

Point.new(x: 111) 
#=> ArgumentError 

, которые могли бы выглядеть примерно так:

module StricterStruct 
    def self.new(*attributes) 
    klass = Class.new 
    klass.instance_eval { ... } 

    klass 
    end 
end 

Но что должно идти в скобки, чтобы определить initialize метод на klass, что:

  • Требуется наличие аргументов ключевого слова без значения по умолчанию;
  • ключевые слова задаются в виде массива символов в attributes; и
  • метод initialize присваивает их переменным экземпляра одного и того же имени

ответ

6

я завелся с использованием (удивительно Pythonic) стратегии **kwargs, благодаря новым признакам в Руби 2.0+:

module StricterStruct 
    def self.new(*attribute_names_as_symbols) 
    c = Class.new 
    l = attribute_names_as_symbols 

    c.instance_eval { 
     define_method(:initialize) do |**kwargs| 
     unless kwargs.keys.sort == l.sort 
      extra = kwargs.keys - l 
      missing = l - kwargs.keys 

      raise ArgumentError.new <<-MESSAGE 
      keys do not match expected list: 
       -- missing keys: #{missing} 
       -- extra keys: #{extra} 
      MESSAGE 
     end 

     kwargs.map do |k, v| 
      instance_variable_set "@#{k}", v 
     end 
     end 

     l.each do |sym| 
     attr_reader sym 
     end 
    } 

    c 
    end 
end 
+0

Мне нравится сообщение, которое я только что изменил, чтобы иметь очень похожую структуру, хотя я дал вам методы getter и setter для экземпляра_переменных. Извините, я был не так быстр, как вы отвечали на свой вопрос :). Также обратите внимание, что я обновил свой ответ, чтобы принять строки или символы и отформатировать их как snake_case. – engineersmnky

+1

Не хотите сказать 'def self.new (* attribute_names_as_symbols)'? Кроме того, я считаю, что вы можете создать каждый аксессуар для чтения с помощью 'attr_reader sym'. Интересный вопрос. –

+0

@CarySwoveland Да, это опечатка моей копии. Спасибо, что поймал это. Вы также правы, что attr_reader намного проще; Я должен был это понять! –

1

я мог бы быть недопонимание вопрос, но вы ищете что-то вроде этого?

module StricterStruct 
    def self.new(*attributes) 
    klass = Class.new 
    klass.class_eval do 
     attributes.map!{|n| n.to_s.downcase.gsub(/[^\s\w\d]/,'').split.join("_")} 
     define_method("initialize") do |args| 
     raise ArgumentError unless args.keys.map(&:to_s).sort == attributes.sort 
     args.each { |var,val| instance_variable_set("@#{var}",val) } 
     end 
     attr_accessor *attributes 
    end 
    klass 
    end 
end 

Тогда

Point = StricterStruct.new(:x,:y) 
#=> Point 
p = Point.new(x: 12, y: 77) 
#=> #<Point:0x2a89400 @x=12, @y=77> 
p2 = Point.new(x: 17) 
#=> ArgumentError 
p2 = Point.new(y: 12) 
#=> ArgumentError 
p2 = Point.new(y:17, x: 22) 
#=> #<Point:0x28cf308 @y=17, @x=22> 

Если вы хотите что-то больше, пожалуйста, объясните, как я думаю, что это соответствует вашим критериям, по крайней мере, мое понимание этого. Поскольку он определяет методы и может принимать аргумент «keyword» (Hash) и назначать соответствующие переменные экземпляра.

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

Также могут быть более чистые реализации.

+0

Это не совсем верно - - как показывает мой пример, 'Point.new (x: 12)' должен был дать вам ArgumentError, поскольку вы не указали 'y'. –

+1

@JohnFeminella пропущенный эта часть. Теперь он выглядит еще более уродливым :(но работает по запросу. – engineersmnky

+0

Не понимаю, почему вы вводите переменную класса, так как 'attributes' видна с помощью' define_method'. –

0

Похоже, что вы ищете рубин встроенный OpenStruct:

require 'ostruct' 

foo = OpenStruct.new(bar: 1, 'baz' => 2) 
foo # => #<OpenStruct bar=1, baz=2> 

foo.bar # => 1 
foo[:bar] # => 1 
foo.baz # => 2 
foo.baz = 3 
foo # => #<OpenStruct bar=1, baz=3> 

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

+0

Как я упоминал в этом примере, одно из желаемых свойств заключается в том, что 'ArgumentError' или подобное исключение возбуждаются, если вы не укажете все. Поскольку «OpenStruct» может быть изменен в любое время, здесь это не очень хороший выбор. Кроме того, OpenStruct намного медленнее, чем Struct, поэтому, если вы их много, это дорого. –

+0

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

0

Я был также охота вокруг этого, и в конце концов наткнулся на этот драгоценный камень, который делает именно это:

https://github.com/etiennebarrie/kwattr

class FooBar 
    kwattr :foo, bar: 21 
end 

foobar = FooBar.new(foo: 42) # => #<FooBar @foo=42, @bar=21> 
foobar.foo # => 42 
foobar.bar # => 21 

вместо

class FooBar 
    attr_reader :foo, :bar 

    def initialize(foo:, bar: 21) 
    @foo = foo 
    @bar = bar 
    end 
end 
Смежные вопросы