Sisss
Sisss

Reputation: 55

Convert embedded blocks into embedded arrays with Ruby

Considering the following code:

class Node
  def initialize(name=nil)
    @name = name
    @children = []
  end

  def node(name, &block)
    child = Node.new(name)
    @children.push(child)
    child.instance_exec(&block) if block
  end
end

def tree(name, &block)
  @tree = Node.new(name)
  @tree.instance_exec(&block)
  @tree
end

t = tree("Simpsons family tree") do
  node("gramps") do
    node("homer+marge") do
      node("bart")
      node("lisa")
      node("maggie")
    end
  end
end

puts "tree = " + t.inspect

Which is returning:

tree = #<Node:0x007fca1a103268 @name="Simpsons family tree", @children=[#<Node:0x007fca1a103128 @name="gramps", @children=[#<Node:0x007fca1a102fe8 @name="homer+marge", @children=[#<Node:0x007fca1a102ef8 @name="bart", @children=[]>, #<Node:0x007fca1a102e80 @name="lisa", @children=[]>, #<Node:0x007fca1a102e08 @name="maggie", @children=[]>]>]>]>

I would like to know if it was possible to make an update in order to return on-the-fly an embedded array of arrays, without using the @children shared array. I would expect this result:

[
  "Simpsons family tree",
  [
    "gramps",
    [
      "homer+marge",
      [
        "bart",
        "lisa",
        "maggie"
      ]
    ]
  ]
]

Is it possible? Thanks for any suggestions.

Edit: Actually, I would like to do so with basically the same code, but without any @children instance. So I want to remove @children.

Edit 2:

Here is my best result, after some tests:

class Node
  def initialize(name=nil)
    @name = name
  end

  def node(name, &block)
    child = Node.new(name)
    @sub = child.instance_exec(&block) if block

    [
      name,
      @sub
    ].compact
  end
end

def tree(name, &block)
  @tree = Node.new(name)

  [
    name,
    @tree.instance_exec(&block)
  ]
end

t = tree("Simpsons family tree") do
  node("gramps") do
    node("homer+marge") do
      node("bart")
      node("lisa")
      node("maggie")
    end
  end
end

puts t.inspect
# => [
#      "Simpsons family tree",
#      [
#        "gramps",
#        [
#          "homer+marge",
#          [
#            "maggie"
#          ]
#        ]
#      ]
#    ]

But there's still a trouble with the flatten nodes. Because only the last one is returned by Ruby.

Upvotes: 1

Views: 144

Answers (1)

Jim Gay
Jim Gay

Reputation: 1488

The formatting of mine isn't exactly what you want, but that's not really the important part.

If you allow your nodes to be initialized with a collection of children, you can have your node method return a new node each time it is called.

class Node
  def self.new(*args, &block)
    instance = super
    if block
      instance.instance_eval(&block)
    else
      instance
    end
  end

  def initialize(name, children=[])
    @name = name
    @children = children
  end

  attr_reader :children, :name

  def node(name, &block)
    new_node = Node.new(name)
    new_node = new_node.instance_eval(&block) if block
    Node.new(self.name, next_children + [new_node])
  end

  def next_children
    children.map{|child| Node.new(child.name, child.next_children) }
  end

  def inspect
    return %{"#{name}"} if children.empty?
    %{"#{name}", #{children}}
  end
end

t = Node.new("Simpsons family tree") do
  node("gramps") do
    node("homer+marge") do
      node("bart").
      node("lisa").
      node("maggie")
    end
  end
end

puts t.inspect
#=> "Simpsons family tree", ["gramps", ["homer+marge", ["bart", "lisa", "maggie"]]]

By changing the behavior of initialization and of the node method you can accumulate the nodes as they are created.

I also took the liberty to remove your tree method since it was just a wrapper for initializing the nodes, but that might be a place to bring back formatting.

I came across this post looking for examples of good use of instance_exec for my Ruby DSL Handbook but you can either use instance_exec or instance_eval for the block since you're not passing any arguments to it.

EDIT: I updated the approach to return new values each time. The change required that nodes without blocks be chained together because each node call return a new object. The return value for the block is a new node.

In order to get the formatting you want, you'd need to do [t].inspect

Upvotes: 0

Related Questions