Home > Blockchain >  How to implement a DSL to dynamically create new methods via method chains?
How to implement a DSL to dynamically create new methods via method chains?

Time:06-19

I have a class and an instance of that class:

class Thing
   def initialize
      @name = name
   end
end

a = Thing.new("a")

I want to dynamically create methods and set their return values by calling a DSL like this:

If I write a.is_a.person:

  • a person? method should be created for a
  • a.person? should return true

If I write a.is_not_a.man

  • a man? method should be created for a
  • a.man? should return false

If I write a.is_the.parent_of.joe

  • a parent_of method should be created for a
  • a.parent_of should return joe

I tried doing this

class Thing
  attr_reader :name

  def initialize(name)
    @name = name
  end

  def is_a
    Class.new do
      def initialize base
        @base = base
      end
      
      def method_missing name
        @base.define_singleton_method "#{name}?" do
            true
        end
      end
    end.new self
  end

  def is_not_a
    Class.new do
      def initialize base
        @base = base
      end
      
      def method_missing name
        @base.define_singleton_method "#{name}?" do
          false
        end
      end
    end.new self
  end
end

And it works for is_a and is_not_a as defined above:

jane = Thing.new('Jane')

jane.is_a.person
jane.person? #=> true

jane.is_a.woman
jane.woman? #=> true

jane.is_not_a.man
jane.man? #=> false

But for the jane.is_the.parent_of.joe, like more deep chaining ones, I'm not able to figure out how I can implement it.

How do I approach this?

CodePudding user response:

With Class.new you create a new anonymous class each time is_a or is_not_a is called. Although this works, there's a much more lightweight approach. You can create a helper that will invoke a given block each time it receives a message:

class MethodCallback
  def initialize(&block)
    @block = block
  end

  def method_missing(name, *args)
    @block.call(name, *args)
  end
end

Example usage:

m = MethodCallback.new { |name| puts "#{name} was called"}

m.foo
# foo was called

m.bar
# bar was called

With this helper, your class can be simplified like this:

class Thing
  def initialize(name)
    @name = name
  end

  def is_a
    MethodCallback.new do |name|
      define_singleton_method(:"#{name}?") { true }
    end
  end

  def is_not_a
    MethodCallback.new do |name|
      define_singleton_method(:"#{name}?") { false }
    end
  end
end
jane = Thing.new('Jane')

jane.is_a.person
jane.person? #=> true

jane.is_a.woman
jane.woman? #=> true

jane.is_not_a.man
jane.man? #=> false

Now, for the chaining one jane.is_the.parent_of.joe, instead of defining a new method right-away, we have to invoke / return another MethodCallback instance in order to get an extra level of method chaining:

class Thing
  # ...
  
  def is_the
    MethodCallback.new do |name|
      MethodCallback.new do |value|
        define_singleton_method(name) { value.to_s }
      end
    end
  end
end

In the above code, the first method name after is_the will be assigned to name and the second will be assigned to value. The method we create will be called just like name and it will return value:

jane.is_the.parent_of.joe
#           ^^^^^^^^^ ^^^
#           name      value
jane.parent_of #=> 'joe'

Another one:

jane.is_the.major_of.london
jane.major_of #=> 'london'

CodePudding user response:

If you are not stuck on using method chaining to define your object, there are lots of alternatives. Here is one that uses a "builder" approach.

class Thing

   attr_reader :name
   def self.define(name, &block)
      thing = Thing.new(name)
      thing.instance_eval(&block)
      thing
   end

   def initialize(name)
      @name = name
   end

   def is(str)

      case str
      when /^\s*a\s (\w )\s*$/
         define_a($1)
      when /^\s*not\s a\s (\w )\s*$/
         define_not_a($1)
      when /^\s*the\s*(\w )\s of\s (\w )\s*$/
         define_relationship($1, $2)
      end

   end

   def define_a(str)
      define_singleton_method("#{str}?".to_sym) {true} 
   end

   def define_not_a(str)
      define_singleton_method("#{str}?".to_sym) {false} 
   end

   def define_relationship(relation, to)
      define_singleton_method("#{relation}_of".to_sym) {to} 
   end
end

a = Thing.define("a") do 
   is "a person"
   is "not a man"
   is "the parent of joe"
end

puts a.name       # => a
puts a.person?    # => true
puts a.man?       # => false
puts a.parent_of  # => joe
  • Related