3

I am using the following code to enforce context of DSL nested constructs. What are the other ways of achieving the same functionality?

def a &block
  p "a"
  def b &block
    p "b"
    def c &block
      p "c"
      instance_eval &block
    end 
    instance_eval &block
    undef :c
  end 
  instance_eval &block 
  undef :b
end 
# Works
a do
  b do
    c do
    end
  end
end

# Doesn't Work 
b do
end
c do
end

Source

I'm Mo
  • 131
  • 8
  • Normally you do `instance_eval` on some kind of proxy object for each level here. Calling `def` inside a method is generally bad form as the results are not always as expected. – tadman Nov 16 '16 at 20:17
  • Welcome to SO. "What are the other ways..." is extremely broad. – the Tin Man Nov 16 '16 at 20:47
  • @tadman could you please elaborate with an example? – I'm Mo Nov 16 '16 at 21:31
  • @theTinMan "the best way" is too subjective. And it is interesting to see the varieties that ruby metaprogramming has to offer. – I'm Mo Nov 16 '16 at 21:31

1 Answers1

3

You asked about other ways, not the best way. So here's some examples :

Example A

class A
  def initialize
    p "a"
  end

  def b &block
    B.new.instance_eval &block
  end
end

class B
  def initialize
    p "b"
  end

  def c &block
    C.new.instance_eval &block
  end
end

class C
  def initialize
    p "c"
  end
end

def a &block
  A.new.instance_eval &block
end

Example B

A bit shorter :

def a &block
  p "a"
  A.new.instance_eval &block
end

class A
  def b &block
    p "b"
    B.new.instance_eval &block
  end

  class B
    def c &block
      p "c"
      C.new.instance_eval &block
    end

    class C
    end
  end
end

Example C

If you don't plan to have a d method for an A::B::C object :

def a &block
  p "a"
  A.new.instance_eval &block
end

class A
  def b &block
    p "b"
    B.new.instance_eval &block
  end

  class B
    def c &block
      p "c"
      instance_eval &block
    end
  end
end

Example D

This was a fun one :

def new_class_and_method(klass_name, next_klass=Object)
  dynamic_klass = Class.new do
    define_method(next_klass.name.downcase){|&block| p next_klass.name.downcase; next_klass.new.instance_eval &block}
  end
  Object.const_set(klass_name, dynamic_klass)
end

new_class_and_method("A", new_class_and_method("B", new_class_and_method("C")))

def a &block
  p "a"
  A.new.instance_eval &block
end

Example E

I dare say this doesn't look half bad:

def new_method_and_class(x)
  define_method(x) do |&block|
    p x
    self.class.const_get(x.capitalize).new.instance_eval &block
  end

  self.const_set(x.capitalize, Class.new)
end

["a", "b", "c"].inject(Object){|klass,x| klass.instance_eval{new_method_and_class(x)} }

Example F

A bit more robust :

def new_method_and_class(x, parent_klass = Object)
  parent_klass.class_eval do
    define_method(x) do |&block|
      p x
      parent_klass.const_get(x.capitalize).new.instance_eval &block if block
    end
  end

  parent_klass.const_set(x.capitalize, Class.new)
end

["a", "b", "c"].inject(Object){|klass,x| new_method_and_class(x,klass) }

Explanation

Example B

In example B, we first define :

  • an a() method
  • an A class

both are defined in main, because we want a() to be available directly. a() method doesn't do much expect printing "a" and passing a block to an instance of A.

Then comes b() method. We don't want it to be available from main, so we define it inside A class. We want to continue with the nested methods, so we define a B class, which is also defined inside A. The B class is actually a A::B class. The A::B#b() method also prints "b", and passes a block to an instance of B.

We continue with A::B::C inside of A::B, just like we did with A::B and A.

Example F

Example F is basically like Example B, but written dynamically.

In example B, we defined an x method and an X class in every step, with the exact same structure. It should be possible to avoid code repetition with a method called new_method_and_class(x) which uses define_method, const_set and Class.new :

new_method_and_class("a") # <- Object#a() and A are now defined

a do
  puts self.inspect
end
#=> "a"
#   <A:0x00000000e58bc0>

Now, we want to define a b() method and a B class, but they shouldn't be in main. new_method_and_class("b") wouldn't do. So we pass an extra parameter, called parent_klass, which defaults to Object :

parent_klass = new_method_and_class("a")
new_method_and_class("b", parent_klass)

a do 
  b do
    puts self.inspect
  end
end

# => "a"
#    "b"
#    <A::B:0x00000000daf368>

b do
  puts "Not defined"
end

# => in `<main>': undefined method `b' for main:Object (NoMethodError)

To define the c method, we just add another line :

parent_klass = new_method_and_class("a")
parent_klass = new_method_and_class("b", parent_klass)
parent_klass = new_method_and_class("c", parent_klass)

And so on and so on.

To avoid code repetition, we can use inject with the parent_klass as accumulator value :

["a", "b", "c"].inject(Object){|klass,x| new_method_and_class(x,klass) }

Bonus - Example G

Here's a modified code from Example F which works with a basic tree structure.

# http://stackoverflow.com/questions/40641273/ruby-dsl-nested-constructs/40641743#40641743
def new_method_and_class(x, parent_klass = Object)
  parent_klass.class_eval do
    define_method(x) do |&block|
      p x.to_s
      parent_klass.const_get(x.capitalize).new.instance_eval &block if block
    end
  end

  parent_klass.const_set(x.capitalize, Class.new)
end

def create_dsl(branch,parent_klass = Object)
  case branch
  when Symbol, String
    new_method_and_class(branch,parent_klass)
  when Array
    branch.each do |child|
      create_dsl(child, parent_klass)
    end
  when Hash
    branch.each do |key, value|
      create_dsl(value, new_method_and_class(key,parent_klass))
    end
  end
end

methods_tree = {
  :a => {
    :b => [
      :c,
      :d
    ],
    :e => :f,
    :g => nil
  }
}

create_dsl(methods_tree)

a do 
  b do
    c do
      puts self.inspect
    end

    d do
    end
  end

  e do
    f do
    end
  end

  g do
    puts self.inspect
  end
end

# => 
#   "a"
#   "b"
#   "c"
#   #<A::B::C:0x0000000243dfa8>
#   "d"
#   "e"
#   "f"
#   "g"
#   #<A::G:0x0000000243d918>
Eric Duminil
  • 52,989
  • 9
  • 71
  • 124
  • 1
    I love the `next_klass` alternative – I'm Mo Nov 16 '16 at 21:37
  • There's a newer version. – Eric Duminil Nov 16 '16 at 22:15
  • is it a known pattern? – I'm Mo Nov 17 '16 at 04:47
  • I don't know. I just began with your example and started coding. I'd be interested to know what other solutions look like. By the way, the last solutions work with arrays, I'll modify them to work with tree. For example if you want to have `c` and `d` methods in `b`. – Eric Duminil Nov 17 '16 at 08:12
  • Sweet!! I am also experimenting with your Example F. To avoid polluting the Object class, trying to make a variant via `inject(MyKlass.singleton_class)` btw a few explanatory hint comments for Example F would be much appreciated. Thanks again :) – I'm Mo Nov 17 '16 at 10:09
  • Will do. Note that in Object, only A class and a() method will be defined. B class and b() method will be defined in A, and C class and c() method will be defined in A::B. – Eric Duminil Nov 17 '16 at 10:42
  • Thanks for the update Eric, you pretty much nailed it!! Amazing!! – I'm Mo Nov 17 '16 at 16:44
  • very nice!! love it! – zotherstupidguy Dec 05 '16 at 18:11
  • @EricDuminil already did! have you considered doing a version where modern ruby features come into play maybe refinements! – zotherstupidguy Dec 06 '16 at 12:19