Setting instance variables from hash parameters (with defaults)

L

Leslie Viljoen

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?
 
L

Leslie Viljoen

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?
 
D

David A. Black

Hi --

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?

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)
 
J

Jesús Gabriel y Galán

I see this is a shorter way:

module Defaulting
=A0 =A0 =A0 =A0def set_params(defs, params)
=A0 =A0 =A0 =A0 =A0 =A0 =A0 =A0defs.merge(params).each {|name, val| eval = "@#{name} =3D val"}
=A0 =A0 =A0 =A0end
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.
 
D

David Masover

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.
 
B

Brian Candler

David said:
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 :)
 
I

Intransition

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 =3D {
:select_max =3D> 10000,
:select_try =3D> 1000,
:min_sleep_sec =3D> 5,
:max_sleep_sec =3D> 1800,
:default_sleep =3D> 10
}

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

# document me

attr_accessor :select_max

# or, if you need more control

def select_max=3D(val)
@select_max =3D val
end

# etc...

T.
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,769
Messages
2,569,579
Members
45,053
Latest member
BrodieSola

Latest Threads

Top