2016-12-13 1 views
0

рубин имеет поддержку autovivification для хэшей, передав блок в Hash.new:Как реализовать автовивитацию для структур Ruby?

hash = Hash.new { |h, k| h[k] = 42 } 
hash[:foo] += 1 # => 43 

Я хотел бы реализовать autovivification для структур, а также. Это лучшее, что я могу придумать:

Foo = Struct.new(:bar) do 
    def bar 
    self[:bar] ||= 42 
    end 
end 

foo = Foo.new 
foo.bar += 1 # => 43 

и, конечно, это только autovivifies именованный аксессор (foo.bar), а не [] форма (foo[:bar]). Есть ли лучший способ реализовать автовивификацию для структур, в частности тот, который работает надежно как для форм, так и для foo[:bar]?

+1

Ваши примеры на самом деле не эквивалентны. Вызывается обработчик по умолчанию hash, потому что 'hash' не имеет ключа': foo'. У экземпляра struct 'foo' с другой стороны есть член': bar' с начальным значением 'nil'. – Stefan

+0

@Stefan Спасибо, это очень верно. И на самом деле это ключ к тому, что мне нужно делать: мне не нужны ленивые аспекты автовивитации, просто нужно, чтобы некоторые поля были настроены так, чтобы они были не ноль. И получается, что настройка метода 'initialize' для настройки начальных значений полей сделает именно это. Еще раз спасибо. :-) –

ответ

3

Я хотел бы пойти со следующим подходом:

module StructVivificator 
    def self.prepended(base) 
    base.send(:define_method, :default_proc) do |&λ| 
     instance_variable_set(:@λ, λ) 
    end 
    end 
    def [](name) 
    super || @λ && @λ.() # or more sophisticated checks 
    end 
end 

Foo = Struct.new(:bar) do 
    prepend StructVivificator 
end 

foo = Foo.new 
foo.default_proc { 42 } # declare a `default_proc` as in Hash 

foo[:bar] += 1 # => 43 
foo.bar += 1  # => 44 

foo.bar выше называет foo[:bar] под капотом через method_missing магии, поэтому единственное, что нужно переписать это Struct#[] метод.

Подготовка модуля делает его более надежным, для каждого экземпляра и в целом более гибким.


Код, приведенный выше, является лишь примером. Для того, чтобы скопировать поведение Hash#default_proc можно было бы (кредитов @Stefan для комментариев):

module StructVivificator 
    def self.prepended(base) 
    raise 'Sorry, structs only!' unless base < Struct 

    base.singleton_class.prepend(Module.new do 
     def new(*args, &λ) # override `new` to accept block 
     super(*args).tap { @λ = λ } 
     end 
    end) 
    base.send(:define_method, :default_proc=) { |λ| @λ = λ } 
    base.send(:define_method, :default_proc) { |&λ| λ ? @λ = λ : @λ } 

    # override accessors (additional advantage: performance/clarity) 
    base.members.each do |m| 
     base.send(:define_method, m) { self[m] } 
     base.send(:define_method, "#{m}=") { |value| self[m] = value } 
    end 
    end 
    def [](name) 
    super || default_proc && default_proc.(name) # or more sophisticated checks 
    end 
end 

Теперь default_proc лямбда получит name решить, как вести себя в таком случае.

Foo = Struct.new(:bar, :baz) do 
    prepend StructVivificator 
end 

foo = Foo.new 
foo.default_proc = ->(name) { name == :bar ? 42 : 0 } 
puts foo.bar   # => 42 
puts foo[:bar] += 1 # => 43 
puts foo.bar += 1  # => 44 
puts foo[:baz] += 1 # => 1 
+1

Обратный вызов 'prepended' и' send' не нужны. Вы можете определить метод с помощью 'def default_proc (&λ); @ λ = λ; end', как и' [] '. И вам даже не нужен другой модуль, определяющий методы в' Foo' (т. Е. Внутри struct 'do ... end_ block) также работает. – Stefan

+0

@Stefan да, я знаю. Был «base.singleton_class.send (: define_method,: new)», который копирует поведение «Hash :: new», назначая по умолчанию proc. Я, наконец, удалил его для ясности. Модуль необходим, если существует более чем одна структура, подлежащая оживлению. Кроме того, я все еще нахожу подход «расширяться в обратном вызове, объявлять методы в модуле тела» более понятным, но это определенно вопрос о вкусе – mudasobwa

+0

И просто переопределить '[]' недостаточно, если вы сначала вызываете 'foo.bar', он возвращает' nil'. Вы также должны переопределить метод getter для каждого члена. – Stefan

Смежные вопросы