I have a class with a constructor that takes two parameters and sets the fields to those values. I want to also be able to initialize objects from this class by setting fields explicitly inside of a block passed to the constructor.

The constructor:

class Grammar
    attr_reader :rules, :start, ...
    def initialize(rules, start)
        @rules = rules
        @start = start
        ...
    end
end

The parameters passed to the constructor involve creating several objects that are only used as intermediate building blocks for the parameters rules and start and it would make sense to limit the existence of these objects to the block passed to the constructor. In this block I would expect to first build the intermediate objects and then directly set the fields of the new object.

I want to be able to instantiate Grammar like so:

grammar = Grammar.new { |g|
    ...
    # build rules and start
    ...
    g.start = start
    g.rules = rules
}

How should I modify the constructor to allow both methods of initialisation?

2 Answers

5
tadman On Best Solutions

It's actually really easy to add this functionality to most classes provided they already have attr_writer and/or attr_accessor in place:

class Grammar
  attr_accessor :rules, :start, ...

  def initialize(rules, start)
    @rules = rules
    @start = start
    ...

    yield self if block_given?
  end
end

Where you can now do exactly what you wanted. The yield self part will supply the object being initialized to the block, and block_given? is only true if you've supplied a block to the new call. You'll want to run this after setting all your defaults.

2
Josh Brody On

You might be looking for Object#Tap which is built-in to Ruby — yields self to the block, and then returns self.

class Person
  attr_accessor :name 
end

person = Person.new.tap do |p|
  p.name = "Jan"
end

puts person.name # hi