Setting instance variables from hash parameters (with defaults)

Hi!

I’m sure I might be reinventing the wheel here.
I was writing the following ugly code in order to set options (with
defaults) from a hash:

def initialize(opts = {})
@select_max = opts.has_key?(:select_max) ? opts[:select_max] : 10000
@select_try = opts.has_key?(:select_try) ? opts[:select_try] : 1000
@min_sleep_sec = opts.has_key?(:min_sleep_sec) ?
opts[:min_sleep_sec] : 5
@max_sleep_sec = opts.has_key?(:max_sleep_sec) ?
opts[:max_sleep_sec] : 1800
@default_sleep = opts.has_key?(:default_sleep) ?
opts[:default_sleep] : 10

So to avoid that I wrote:

module Defaulting
def set_params(defs, params)
defs.each do |name, val|
if params.has_key?(name)
eval “@#{name} = params[name]”
else
eval “@#{name} = val”
end
end
end
end

So that I could do the much nicer:

include Defaulting
def initialize(opts = {})
defs = {
:select_max => 10000,
:select_try => 1000,
:min_sleep_sec => 5,
:max_sleep_sec => 1800,
:default_sleep => 10
}
set_params(defs, opts)

Note that this allows me to pass options that are explicitly set to nil.

Now:

  1. Is this functionality already tucked away somewhere else?
  2. How can I get rid of those nasty evals?

I see this is a shorter way:

module Defaulting
def set_params(defs, params)
defs.merge(params).each {|name, val| eval “@#{name} = val”}
end
end

…still, any way to get rid of that eval?

Whoops, correction:

On Tue, 27 Oct 2009, David A. Black wrote:

def initialize(opts)
DEFAULTS.update(opts).each do |name, value|

That has to be merge, not update.

David


The Ruby training with D. Black, G. Brown, J.McAnally
Compleat Jan 22-23, 2010, Tampa, FL
Rubyist http://www.thecompleatrubyist.com

David A. Black/Ruby Power and Light, LLC (http://www.rubypal.com)

On Mon, Oct 26, 2009 at 4:42 PM, Leslie V.
[email protected] wrote:

I see this is a shorter way:

module Defaulting
def set_params(defs, params)
defs.merge(params).each {|name, val| eval “@#{name} = val”}
end
end

…still, any way to get rid of that eval?

One issue with this approach is that with params you can get arbitrary
instance variables injected, while with the other approach you were
restricting to the ones you had in defaults. This might be a problem
or not…

To get rid of the eval, use instance_variable_set.

Jesus.

Hi –

On Tue, 27 Oct 2009, Leslie V. wrote:

@max_sleep_sec = opts.has_key?(:max_sleep_sec) ? opts[:max_sleep_sec] : 1800
  else
defs = {

Note that this allows me to pass options that are explicitly set to nil.

Now:

  1. Is this functionality already tucked away somewhere else?
  2. How can I get rid of those nasty evals?

Starting with #2: you can always do:

instance_variable_set(“@#{name}”, value)

For the initialize thing, I would probably do something like this:

class Whatever
DEFAULTS = {
:select_max => 10000,
:select_try => 1000,
:min_sleep_sec => 5,
:max_sleep_sec => 1800,
:default_sleep => 10
}

 def initialize(opts)
   DEFAULTS.update(opts).each do |name, value|
     instance_variable_set("@#{name}", value)
   end
 end

end

Another thing to keep in mind for similar cases is that hashes return
nil (unless you override the default) for non-existent keys. So,
unless you have a hash where nil might be a valid value, you can do:

h[x] ||= y

rather than checking for a key.

David


The Ruby training with D. Black, G. Brown, J.McAnally
Compleat Jan 22-23, 2010, Tampa, FL
Rubyist http://www.thecompleatrubyist.com

David A. Black/Ruby Power and Light, LLC (http://www.rubypal.com)

On Monday 26 October 2009 10:42:01 am Leslie V. wrote:

I see this is a shorter way:

module Defaulting
def set_params(defs, params)
defs.merge(params).each {|name, val| eval “@#{name} = val”}
end
end

…still, any way to get rid of that eval?

You want instance_variable_set:

module Defaulting
def set_params def, params
defs.merge(params).each {|name, val| instance_variable_set name,
val}
end
end

I can think of a few ways to make it easier to use, and I’d suggest
actually
calling the name= method, rather than setting the instance variable
directly.
Here’s a rough sketch:

module Defaulting
def init_with_vars *vars, &init_block
defaults = vars.last.kind_of?(Hash) ? vars.pop : {}
vars = (vars + defaults.keys).uniq
attr_accessor *vars
define_method :initialize do |*args, &block|
if args.last.kind_of? Hash
defaults.merge(args).each_pair {|name, val|
self.send("#{name}=", val)
}
end
if init_block
init_block.call *args, &block
end
end
end
end

I’m fairly sure there’s something like this already, some combination of
something like Struct could work. I do like this usage, though:

class Foo
include Defaulting
init_with_vars :foo, :bar, :baz => ‘default baz’
def bar
‘overriding default bar reader’
end
end

Mostly because for so many classes, I don’t need an initialize method at
all,
except as a convenience to set up variables I know I’ll need.

David A. Black wrote:

For the initialize thing, I would probably do something like this:

class Whatever
DEFAULTS = {
:select_max => 10000,
:select_try => 1000,
:min_sleep_sec => 5,
:max_sleep_sec => 1800,
:default_sleep => 10
}

 def initialize(opts)
   DEFAULTS.update(opts).each do |name, value|
     instance_variable_set("@#{name}", value)
   end
 end

end

Note: if you want subclasses to be able override the DEFAULTS, then use
self.class::DEFAULTS instead of just DEFAULTS. Otherwise DEFAULTS will
statically resolve to Whatever::DEFAULTS.

You might also want to add DEFAULTS.freeze, to prevent you accidentally
mucking them up (as ‘update’ does :slight_smile:

On Oct 26, 11:32 am, Leslie V. [email protected] wrote:

  1. Is this functionality already tucked away somewhere else?
  2. How can I get rid of those nasty evals?

Facets has #instance_assign, however the library is transitioning to
the more flexible instance_vars.update().

What I usually do of this kind of thing is use setter methods. That
way you can control what comes in, how it comes in, and bonus! it’s
well documented. Eg. Along the lines of:

     DEFAULTS = {
       :select_max => 10000,
       :select_try => 1000,
       :min_sleep_sec => 5,
       :max_sleep_sec => 1800,
       :default_sleep => 10
     }

     def initialize(opts = {})
       DEFAULTS.merge(opts).each do |k,v|
         send("#{k}=", v)
       end
     end

     # document me

     attr_accessor :select_max

     # or, if you need more control

     def select_max=(val)
       @select_max = val
     end

     # etc...

T.

Thanks for the replies!