Search code examples
rubyoophashinitialization-list

Ruby Hash initialize confusion


I have two classes.

class Sky
  attr_accessor :args
  def initialize(args)
    @args = args
    puts 'Initializing sky'
  end
end


class ShadowMask
  attr_accessor :sky

  def initialize(args)
    args.each{|k, v| p "#{k}: #{v.to_s}"}
    @sky = args.fetch(:sky, Sky.new({}))
  end
end

ShadowMask can be created either with a default Sky:

sm_default = ShadowMask.new({})
# Initializing sky
# => #<ShadowMask:0x007fa215230eb0 @sky=#<Sky:0x007fa215230e60 @args={}>>
sm_default.sky
# => #<Sky:0x007fa215230e60 @args={}>
sm_default.sky.args
# => {}

or with a Sky that has been previously created:

skyobj = Sky.new("Sky Object")
# Initializing sky
# => #<Sky:0x007fa21481a020 @args="Sky Object">
sm = ShadowMask.new(:sky => skyobj)
# "sky: #<Sky:0x007fa21481a020>"
# Initializing sky
# => #<ShadowMask:0x007fa21521ae80 @sky=#<Sky:0x007fa21481a020 @args="Sky Object">>

In this second case, the instance of Sky already exists, and I do not want to see the output Initializing sky from the Sky initialization.

The problem with my actual code is that

puts 'Initializing sky'

is a call to a method that performs several calculations to complete the initialization and set several attributes. And this is repeated without need every time a ShadowMask is created.

Interestingly, if I replace

@sky = args.fetch(:sky, Sky.new({}))

with something like

@sky = args.fetch(:sky, 'AnyString')

it works fine, but I would loose the possibility of creating a new Sky if needed.

I am not sure if the problem is in the syntax or I am making a conceptual mistake.


Solution

  • If all you want is to provide a default Sky object when key :sky is omitted, then fetch is a sub-optimal choice. This will work better:

    @sky = args[:sky] || Sky.new({})
    

    The "problem" with fetch is that this will result in a nil sm.sky:

    sm = ShadowMask.new(sky: nil)
    

    If this is desired behaviour for you, then use fetch. If not, use ||.