Writing proper getter in a Ruby way

B

Benoit Daloze

Hi Rubyists !

While writing a 'getter'(it actually does more than just getting a
@var's value) method, I was wondering what was the most ruby-way to
write it.

The 'issue' is
if the @var is not initialized in constructor, and you access it
directly like "@var",
Ruby gives you a warning(only if you enable them of course) :
"warning: instance variable @value not initialized"

While "@var" returns nil as expected, it still show this warning.
Well, I know you would probably not care too much about warnings(do
you?), but how would you deal to make it disappear?

If you use attr_* methods, you'll never see this warning, because it's
"cheating" (For what I understood of the C code, it checks if
defined).

In fact, a ruby equivalent(for the user) of
attr_reader :var
is
def var
instance_variable_defined?:)@var) ? @var : nil
end
and not just "@var" (because of the warning).

I think you will agree with me, using #instance_variable_defined? is
not very nice.

Now I begin to describe the real code, because abstract examples would be hard.
Here it is:

class Variable
attr_accessor :name, :proc
attr_writer :value

# Create a new Variable, with optional name, value and proc
def initialize(*args, &proc)
args.each do |arg|
case arg
when Numeric then @value = arg
when String, Symbol then @name = arg.to_s
else raise ArgumentError, "..."
end
end
@proc = proc
end

def value
@value or @proc && @proc.call.value
end

def to_s
name || 'unnamed_variable'
end
end

So the constructor have optional *args, and so we are not sure if a
String or Symbol will be given for the name, neither if the value will
be provided.

For #to_s, I used the accessor(getter) of @name ("name"), that avoid
me the warning(instead of @name || ...)

But for value, it's a bit complicated. Because we want to return or
the value if given, or the result of the proc.
I can't use the accessor of value, because we are in a method with the
same name ...

There are then many possibles:
1) Initialize @value in #initialize : "@value = nil" or "@value =
unnamed_variable" (but it looks so old school)
2) Verify if @value is defined with #instance_variable_defined? :
instance_variable_defined?:)@value) ? @value : @proc &&
@proc.call.value (but it's awful to read, btw #defined? still show the
warning)
3) We don't care about warnings !
4) bad names involve this conflict, we should rename @value (we can't
change the name of the method), and then use his accessor. The main
problem is we'll have then something like var.val = 2, which is less
easy to understand than var.value = 2.
5) Another idea ? :D

Excuse me if I'm writing very much for a very small problem, but I
thank you already if you take time to read and/or reply ;)

Regards,
B.D.
 
M

Marnen Laibow-Koser

Benoit Daloze wrote:
[...]
Now I begin to describe the real code, because abstract examples would
be hard.
Here it is:

class Variable
attr_accessor :name, :proc
attr_writer :value

# Create a new Variable, with optional name, value and proc
def initialize(*args, &proc)
args.each do |arg|
case arg
when Numeric then @value = arg
when String, Symbol then @name = arg.to_s
else raise ArgumentError, "..."
end
end
@proc = proc
end

This seems a strange use case, but in any event, ou should not be doing
this much class checking. I suggest a different approach entirely: pass
a hash to the constructor rather like many Rails functions do.

So:

class Variable
attr.accessor :name, :proc
attr.writer :value

def initialize(options, &proc)
@name = options[:name] ? options[:name].to_s : nil
@value = options[:value]
@proc = proc
end
def value
@value or @proc && @proc.call.value
end

def to_s
name || 'unnamed_variable'
end
end

...and these can stay the same.


Best,
 
B

Benoit Daloze

2010/1/24 Marnen Laibow-Koser said:
This seems a strange use case, but in any event, ou should not be doing
this much class checking. =A0I suggest a different approach entirely: pas= s
a hash to the constructor rather like many Rails functions do.

So:

class Variable
=A0attr.accessor :name, :proc
=A0attr.writer :value

=A0def initialize(options, &proc)
=A0 =A0@name =3D options[:name] ? options[:name].to_s : nil
=A0 =A0@value =3D options[:value]
=A0 =A0@proc =3D proc
=A0end
=A0 def value
=A0 =A0 @value or @proc && @proc.call.value
=A0 end

=A0 def to_s
=A0 =A0 name || 'unnamed_variable'
=A0 end
end

...and these can stay the same.

Thank you for your answer. I know that class checking is not so OO.
The old behavior was like you propose: using a Hash. and then we were using
@name, @value =3D options.values_at:)name, :value) # values_at is really
nice here :)

That's a very cool approach except it's really longer and I think it's
significant in this context.
We are writing a math library, this class represent a mathematical
variable, so here we have:

x =3D var :x, 3 # with my way
x =3D var name: :x, value: 3 # with hash, in 1.9
x =3D var :name =3D> :x, :value =3D> 3 # with hash, in 1.8 (not relevant)
To specify the name is already redundant with the real var name. It's
possible via caller to get it, with parsing, but that's not a very
good way neither. (Would you support that?)

So much checking in a case statement for classes looks bad, but it was
intended to raise an ArgumentError if we still used the old behavior.

So, could I have your opinion, if we forget about Hash because it
looks too long here ?
Thank you for your answer again.
 
T

The Higgs bozo

I assume the usual thing is to assign @value = nil in initialize() and
call it a day.

But I think you are asking a higher-level question: How do we abstract
away this detail?

The attr_* methods define methods which create instance variables upon
first being called. But what if we want those instance variables to be
initialized beforehand?

We would need to override the attr_* methods. But those methods belong
to the class object--how are they going to initialize variables in the
instances? By inserting code into the initialize() ancestor chain.

module InitializeAttr
[:attr_reader, :attr_writer, :attr_accessor].each do |method|
define_method method do |*syms|
super(*syms).tap do
mod = Module.new do
define_method :initialize do |*args, &block|
super(*args, &block).tap do
syms.each do |sym|
instance_variable_set("@#{sym}", nil)
end
end
end
end
include mod
end
end
end
end

class Control
attr_writer :a, :b
def bar
@a.to_i + 5
end
end

control = Control.new
p control.instance_variables
#=> []
p control.bar
#=> warning: instance variable @a not initialized
#=> 5

class Experiment
extend InitializeAttr
attr_writer :a, :b
def bar
@a.to_i + 5
end
end

exp = Experiment.new
p exp.instance_variables
#=> [:mad:a, :mad:b]
p exp.bar
#=> 5
# yay no warnings

Therein lies either an abstraction technique or an overkill technique.

A few things about define_method blocks,

+ The &block argument is unsupported in Ruby 1.8.6. Sell your soul to
eval() for a workaround.

+ Implicit super arguments are broken in 1.8.7. It passes the regular
arguments but forgets about the &block argument.

+ In Ruby 1.9 super *must* take explicit arguments. Don't know why.
 
B

Benoit Daloze

[Note: parts of this message were removed to make it a legal post.]
I assume the usual thing is to assign @value = nil in initialize() and
call it a day.
That's the first option, it just looks not cool at all, but it's very simple
an explicit, sure.
But I think you are asking a higher-level question: How do we abstract
away this detail?

The attr_* methods define methods which create instance variables upon
first being called. But what if we want those instance variables to be
initialized beforehand?

We would need to override the attr_* methods. But those methods belong
to the class object--how are they going to initialize variables in the
instances? By inserting code into the initialize() ancestor chain.

module InitializeAttr
[:attr_reader, :attr_writer, :attr_accessor].each do |method|
define_method method do |*syms|
super(*syms).tap do
mod = Module.new do
define_method :initialize do |*args, &block|
super(*args, &block).tap do
syms.each do |sym|
instance_variable_set("@#{sym}", nil)
end
end
end
end
include mod
end
end
end
end

class Control
attr_writer :a, :b
def bar
@a.to_i + 5
end
end

control = Control.new
p control.instance_variables
#=> []
p control.bar
#=> warning: instance variable @a not initialized
#=> 5

class Experiment
extend InitializeAttr
attr_writer :a, :b
def bar
@a.to_i + 5
end
end

exp = Experiment.new
p exp.instance_variables
#=> [:mad:a, :mad:b]
p exp.bar
#=> 5
# yay no warnings

Therein lies either an abstraction technique or an overkill technique.
Cool way to do that :) but overkill also...
It's very interesting anyway how we can change that.
A few things about define_method blocks,

+ The &block argument is unsupported in Ruby 1.8.6. Sell your soul to
eval() for a workaround.

+ Implicit super arguments are broken in 1.8.7. It passes the regular
arguments but forgets about the &block argument.

+ In Ruby 1.9 super *must* take explicit arguments. Don't know why.

I don't have that:
($ ruby -v #=> ruby 1.9.2dev (2010-01-14 trunk 26319) [x86_64-darwin10.2.0])
class P
def m(*args, &b)
[args, b]
end
end

class C < P
def m(*args, &b)
p [args, b]
super.tap { |sup| p sup }
end
end

C.new.m:)a, :b) { |e| e }
#=> [[:a, :b], #<Proc:...>]
#=> [[:a, :b], #<Proc:...>]
#=> [:a, :b]

So another way, probably less 'overkill' you made me think is:

class Test
attr_reader :var
def initialize(*args)
if args.length > 0
@var = :value
end
end

alias :get_var :var
def var
get_var || 3
end
end

p Test.new.var #=> 3
p Test.new:)arg1, :arg2).var #=> :value

Just aliasing the old accessor, too easy :) (and short, no line lost if we
compare to initialization, and we suppose to have already other attr_*)
I just forgot a moment name conflicts about methods don't exist in Ruby,
because you can so easily copy(alias) the old method.

Thanks for your answer :)
 
M

Marnen Laibow-Koser

Benoit said:
Thank you for your answer. I know that class checking is not so OO.

It's very OO. It's just not very Rubyish.
The old behavior was like you propose: using a Hash. and then we were
using
@name, @value = options.values_at:)name, :value) # values_at is really
nice here :)

That's a very cool approach except it's really longer and I think it's
significant in this context.
We are writing a math library, this class represent a mathematical
variable,

Why do you need a separate class for this, instead of using Ruby's
variable mechanism?
so here we have:

x = var :x, 3 # with my way
x = var name: :x, value: 3 # with hash, in 1.9
x = var :name => :x, :value => 3 # with hash, in 1.8 (not relevant)
To specify the name is already redundant with the real var name. It's
possible via caller to get it, with parsing, but that's not a very
good way neither. (Would you support that?)

No, that seems bad. I think you have three good choices for the
constructor, then:
* Define the arguments positionally so that it's always new(name, value)
* Pass a short hash: new:)x => 3)
* Use method_missing: x = Variable.x(3)
So much checking in a case statement for classes looks bad, but it was
intended to raise an ArgumentError if we still used the old behavior.

Then just check for a Hash!

Or better yet, support both syntaxes.
So, could I have your opinion, if we forget about Hash because it
looks too long here ?

I don't think that's a great reason to drop it, but see above for other
ideas.
Thank you for your answer again.

Best,
 
T

The Higgs bozo

I don't have that:

When I said "A few things about define_method blocks," I meant this,

class P
define_method :m do |*args, &b|
[args, b]
end
end

class C < P
define_method :m do |*args, &b|
p [args, b]
p super
end
end

C.new.m:)a, :b) { |e| e }

Ruby 1.9:
#=> in `block in <class:C>': implicit argument passing of super from
method defined by define_method() is not supported. Specify all
arguments explicitly. (RuntimeError)

Ruby 1.8.7:
So another way, probably less 'overkill' you made me think is:

class Test
attr_reader :var
def initialize(*args)
if args.length > 0
@var = :value
end
end

alias :get_var :var
def var
get_var || 3
end
end

p Test.new.var #=> 3
p Test.new:)arg1, :arg2).var #=> :value

I dunno, it looks convoluted. Might as well go back to "old school"
style, or if it becomes redundant you can use the previously mentioned
technique to abstract like this,

initialized_attr_reader :var, 3

Going further, you can change the '3' into a block evaluated by newly
created instances.

Or maybe Struct is all you need for initializing variables,

Struct.new:)a, :b).new(2, 3)
#=> #<struct #<Class:0x90bdc> a=2, b=3>
Struct.new:)a, :b).new
#=> #<struct #<Class:0x85070> a=nil, b=nil>
 
T

The Higgs bozo

Wait, why couldn't you just call accessors through 'self'?

class Foo
attr_accessor :a, :b
def bar
self.a.to_i + 5
end
end

p Foo.new.bar
#=> 5
# (no warnings)
 
B

Benoit Daloze

[Note: parts of this message were removed to make it a legal post.]
Wait, why couldn't you just call accessors through 'self'?

class Foo
attr_accessor :a, :b
def bar
self.a.to_i + 5
end
end

p Foo.new.bar
#=> 5
# (no warnings)

We are in #value, to get @value ...
class C
attr_reader :var
def var
self.var || 2
end
end

C.new.var #=> SystemStackError: stack level too deep

Anyway, we have chosen sth like:
args.unshift nil if Numeric === args.first
(@name, @value), @proc = args, proc

And then not using the accessor of attr_* for reading.
It's just a quick escape sequence from initialization.

Thanks for all your answers,
Regards,
B.D.
 

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,755
Messages
2,569,534
Members
45,008
Latest member
Rahul737

Latest Threads

Top