Huw Walters
Huw Walters

Reputation: 2186

Yield from superclass bypasses subclass block in Ruby

Suppose I have two classes:

When I run this sample code, the block in Outer is skipped, and I get this weird undefined method error:

# subclassed.rb

class Inner
  def func
    puts '  Inner entered func'
    yield 1
    yield 2
    yield 3
    puts '  Inner leaving func'
  end
end

class Outer < Inner
  def func
    puts 'Outer entered func'
    super.func do |value|
      puts "    Outer received #{value} !!! this never happens !!!"
      yield value
    end
    puts 'Outer leaving func'
  end
end

outer = Outer.new
outer.func do |value|
  puts "      Script received #{value}"
end

$ ruby subclassed.rb
Outer entered func
  Inner entered func
      Script received 1
      Script received 2
      Script received 3
  Inner leaving func
C:/Source/temp/rubytest/subclassed.rb:14:in `func': undefined method `func' for nil:NilClass (NoMethodError)
        from C:/Source/temp/rubytest/subclassed.rb:23:in `<main>'

However, if I use encapsulation, everything works as expected:

# encapsulated.rb

class Inner
  def func
    puts '  Inner entered func'
    yield 1
    yield 2
    yield 3
    puts '  Inner leaving func'
  end
end

class Outer
  def initialize
    @inner = Inner.new # (use encapsulation)
  end
  def func
    puts 'Outer entered func'
    @inner.func do |value| # (use encapsulation)
      puts "    Outer received #{value}"
      yield value
    end
    puts 'Outer leaving func'
  end
end

outer = Outer.new
outer.func do |value|
  puts "      Script received #{value}"
end

$ ruby encapsulated.rb
Outer entered func
  Inner entered func
    Outer received 1
      Script received 1
    Outer received 2
      Script received 2
    Outer received 3
      Script received 3
  Inner leaving func
Outer leaving func

As an experiment, I changed def func to def func(&callback), and yield 1 to callback.call(1), but I got identical behaviour.

There are some existing posts dealing with subclasses and superclasses, and some others dealing with yields. However, I can't find anything on this particular problem. This is the first time I've actually been surprised by something Ruby did in a long time! Anyone have any ideas?

Upvotes: 2

Views: 519

Answers (1)

kiddorails
kiddorails

Reputation: 13014

class Outer < Inner
  def func(&block)
    puts "outer entered func"
    super do |value|
      puts "   Outer received #{value}"
      block.call(value)
    end
    puts "Outer leaving func"
  end
end

I am passing the block with &block and then calling block in context of parent's method. Also, just use super instead of super.func to call parent's method.

Output:

o = Outer.new
o.func do |val|
  puts "   Script received #{val}"
end
outer entered func
  Inner entered func
   Outer received 1
   Script received 1
   Outer received 2
   Script received 2
   Outer received 3
   Script received 3
  Inner leaving func
Outer leaving func

Upvotes: 2

Related Questions