JC_Morph JC_Morph - 6 months ago 28
Ruby Question

return object of different subclass from superclass initialize method in Ruby

So I have multiple subclasses of Thing, represented here as ThingA and ThingB.

A few things should be taken for granted here:


  • a Thing is never created directly - i.e
    Thing.new

  • a ThingA must pass a test when initialized, otherwise it should be a ThingB

  • a ThingB can safely be assumed to be a ThingB



Here is a sketch of my hierarchy:

class Thing
def initialize( var = 'yes' )
@var = var
if !self.verify?
ThingB.new( var )
elsif self.class != ThingB
#code for ThingA
@Aness = 'huge'
end
#code for ThingA & ThingB
puts 'END'
end

def verify?
if self.class == ThingA
@var == 'yes'
else
true
end
end
end

class ThingA < Thing
end

class ThingB < Thing
end


My question is, how can I get

ThingA.new( 'no' )


to return a
ThingB
instead?

This is really annoying me, because I had this working with very similar code, but somehow I lost the desired functionality. With the above, I get the following output:

[21] pry(main)> ThingA.new
END
=> #<ThingA:0x60bd4b0 @Aness="huge", @var="yes">
#this is fine
[22] pry(main)> ThingB.new
END
=> #<ThingB:0x53ba6b8 @var="yes">
#this also
[23] pry(main)> ThingA.new( 'no' )
END
END
=> #<ThingA:0x64bec40 @var="no">
#this should be ThingB


'END' prints twice, implying that a ThingB was initialized, but it does not get returned in lieu of the original ThingA. Instead I have a ThingA with no Aness.

As mentioned, I had extremely similar code that functioned as wanted, without using
throw
or anything - which I somehow broke.

Using
return
only stops the first initialize from ending, and still returns the original object.

Answer

I'm not necessarily advocating that this is the correct way to design your system, but there's two reasons that what you've written doesn't work as you intended it.

First, even in a 'simple' case, the above would never result in the return value being a ThingB; the last line of your initialize method is a puts call, and puts always has a return value of nil, so in the simple case of a 'normal' method, your return value still wouldn't be a ThingB instance, it'd be nil.

But, as you say,

Using return only stops the first initialize from ending, and still returns the original object.

I'm assuming you mean using an explicit return in the initialize method, like this hypothetical code:

class Thing
  def initialize( var = 'yes' )
    @var = var    
    if !self.verify?
      return ThingB.new( var ) # explicit return
    elsif self.class != ThingB
      #code for ThingA
      @Aness = 'huge' 
    end
    #code for ThingA & ThingB
    puts 'END'
  end

  def verify?
    if self.class == ThingA
      @var == 'yes'
    else
      true
    end
  end
end

So why doesn't that work? The answer is subtle, but ultimately simple, and pretty key to understanding Ruby (I think): you're not calling initialize in your code, you're calling new. New can't just return whatever initialize returns, because then your original class definition (without an explicit return) would have made ThingA.new return nil![*]

The way new actually works is more like this:

class Thing
  def self.new(*args)
    obj = self.allocate
    obj.initialize(*args) # sort of; initialize is private
    return obj
  end
end

You'll note that the return value of initialize is completely ignored; that's a good thing, if it weren't we'd have to have every initializer tediously return self, and we'd get errors every time we forgot.

So, if you want ThingA.new to return an instance of ThingB, you don't need to modify ThingA#initialize, you need to modify ThingA.new:

class Thing
end

class ThingA < Thing
  def self.verify?(var)
    var == 'yes'
  end

  def self.new(var = 'yes')
    if self.verify?(var)
      super
    else
      ThingB.new(var)
    end
  end

  def initialize(var)
    @Aness = 'huge'
  end
end

class ThingB < Thing
end

I should stress that this is not necessarily a wise thing to do with your code. But I do think knowing how to do it, and why it works, is important to understanding Ruby.


[*]: Again, not because it lacks an explicit return, but because it implicitly returns the value of the last evaluated expression, which is puts 'END', and puts always returns nil.