Writing proper getter in a Ruby way

Discussion in 'Ruby' started by Benoit Daloze, Jan 23, 2010.

  1. 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.
    Benoit Daloze, Jan 23, 2010
    #1
    1. Advertising

  2. 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,
    --
    Marnen Laibow-Koser
    http://www.marnen.org

    --
    Posted via http://www.ruby-forum.com/.
    Marnen Laibow-Koser, Jan 24, 2010
    #2
    1. Advertising

  3. 2010/1/24 Marnen Laibow-Koser <>:
    > 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.
    Benoit Daloze, Jan 24, 2010
    #3
  4. 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.
    --
    Posted via http://www.ruby-forum.com/.
    The Higgs bozo, Jan 24, 2010
    #4
  5. [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 :)
    Benoit Daloze, Jan 24, 2010
    #5
  6. Benoit Daloze wrote:
    > 2010/1/24 Marnen Laibow-Koser <>:
    >> �def initialize(options, &proc)
    >>> � def to_s
    >>> � � name || 'unnamed_variable'
    >>> � end
    >>> end

    >>
    >> ...and these can stay the same.
    >>

    >
    > 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,
    --
    Marnen Laibow-Koser
    http://www.marnen.org

    --
    Posted via http://www.ruby-forum.com/.
    Marnen Laibow-Koser, Jan 24, 2010
    #6

  7. > 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:
    #=> [[:a, :b], #<Proc:0x000270b0@test/iacc.rb:15>]
    #=> [[:a, :b], nil]

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

    --
    Posted via http://www.ruby-forum.com/.
    The Higgs bozo, Jan 24, 2010
    #7
  8. 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)
    --
    Posted via http://www.ruby-forum.com/.
    The Higgs bozo, Jan 24, 2010
    #8
  9. [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.
    Benoit Daloze, Jan 24, 2010
    #9
    1. Advertising

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

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. jc1771
    Replies:
    2
    Views:
    1,016
    jc1771
    Sep 10, 2003
  2. Raoul Markus
    Replies:
    3
    Views:
    1,075
    Roedy Green
    Sep 20, 2003
  3. Replies:
    1
    Views:
    87
  4. John Small
    Replies:
    2
    Views:
    86
    John Small
    Dec 15, 2008
  5. Hal Fulton
    Replies:
    1
    Views:
    154
    Gregory Brown
    Nov 5, 2009
Loading...

Share This Page