Forum: Ruby Using method missing to create getters and setters

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
Tim C. (Guest)
on 2009-04-26 21:40
Hi,
I am trying to create a model (BatchExternalBooking) which has the
following methods:
BatchExternalBooking.message_thread_1=
BatchExternalBooking.message_thread_1
BatchExternalBooking.message_thread_2=
BatchExternalBooking.message_thread_2
... etc all the way upto
BatchExternalBooking.message_thread_x=
BatchExternalBooking.message_thread_x

I assume that I need to do this via method_missing because I don't know
how many of these methods I will actually need.

My code currently looks like this:
class BatchExternalBooking
  def method_missing(method_sym, *args)
    if method_sym.to_s =~ /^message_thread_([0-9]*)=?(\w*)?$/
      BatchExternalBooking.instance_eval "attr_accessor
:message_thread_#{$1}"
      self.send("message_thread_#{$1}=", $2)
    else
      super
    end
  end
end

However, this is not working.  See below:
>> b = BatchExternalBooking.new
=> #<BatchExternalBooking:0x3e54040>
>> b.message_thread_1 = 45
=> 45
>> b.message_thread_1
=> ""
The value wasn't actually set but the attr_accessor correctly created
the setter method.  If I try and set the variable again, it works:
>> b.message_thread_1 = 45
=> 45
>> b.message_thread_1
=> 45

Why isn't the setter working the first time round?

Thanks a lot.
Robert D. (Guest)
on 2009-04-26 22:50
(Received via mailing list)
Hmm seems a little confusing with the $1 in the evals and $2 is
outright wrong you mean args.first

def method_missing sym, *args
   name = sym.to_s
   aname = name.sub("=","")

   super unless aname =~ /whatever/

   self.class.module_eval do # just a matter of taste
      attr_accessor aname
   end
  send name, args.first unless aname == name
end


HTH
Robert
Tim H. (Guest)
on 2009-04-26 23:29
(Received via mailing list)
Tim C. wrote:
>
>     else
>>> b.message_thread_1
> Thanks a lot.
Two thoughts. First, why not use an array? Then

BatchExternalBooking.message_thread[n] = whatever

If that doesn't work for you, then investigate OpenStruct (i.e.
'ostruct').
Robert K. (Guest)
on 2009-04-26 23:30
(Received via mailing list)
On 26.04.2009 19:40, Tim C. wrote:
>
> I assume that I need to do this via method_missing because I don't know
> how many of these methods I will actually need.
>
> My code currently looks like this:
> class BatchExternalBooking
>   def method_missing(method_sym, *args)
>     if method_sym.to_s =~ /^message_thread_([0-9]*)=?(\w*)?$/
>       BatchExternalBooking.instance_eval "attr_accessor
> :message_thread_#{$1}"

You might rather want to use class_eval here.

Also, as Robert pointed out already, using $1 only once and storing the
value in a local variable is safer because it can be changed by any
method you invoke.

>>> b.message_thread_1 = 45
> Why isn't the setter working the first time round?
>
> Thanks a lot.

Is there a reason that you do not use OpenStruct or an ordinary Array
for this?  It seems you are indexing by number anyway so why not use an
Array?

Kind regards

  robert
Tim C. (Guest)
on 2009-04-26 23:49
The reason that I am trying to use methods/attributes to store the
information rather than an array is because this model is used to create
a batch update form in a Rails project.  If one of the updates fails
then I want to display the errors on the form next to the relevant
fields.  For this to work, I will need to use the error_messages_for
helper to which I need to pass the attribute.

Am I approaching this in the correct manner?
Rick D. (Guest)
on 2009-04-26 23:56
(Received via mailing list)
On Sun, Apr 26, 2009 at 3:30 PM, Robert K.
<removed_email_address@domain.invalid> wrote:
> On 26.04.2009 19:40, Tim C. wrote:

>> My code currently looks like this:
>> class BatchExternalBooking
>>  def method_missing(method_sym, *args)
>>    if method_sym.to_s =~ /^message_thread_([0-9]*)=?(\w*)?$/
>>      BatchExternalBooking.instance_eval "attr_accessor
>> :message_thread_#{$1}"
>
> You might rather want to use class_eval here.

No instance_eval works just as well for sending attr_accessor to a
class/module. The only difference with class_eval is when using def to
define a method.

Both do the evaluation in the context of the receive but class_eval
sets the current class to the receiver.

Class.class_eval("def foo;end") defines an instance method while
Class.instance_eval("def foo;end") defines a class method.

This seems a bit less odd when you consider that a class method is
just a singleton method on the class (albeit one which can be
inherited by subclasses).

>
> Also, as Robert pointed out already, using $1 only once and storing the
> value in a local variable is safer because it can be changed by any method
> you invoke.
>
>>      self.send("message_thread_#{$1}=", $2)

That's not true either. The $n variables are frame local, in a given
invocation frame they will only change when another regex match is
done in the same invocation.  The bug here is that this line should
have been

       self.send("message_thread_#{$1}=", *args)  # or args.first if you
must.

Since the regexp only had a single capture, $2 is always nil, so even
though the newly created setter is geting called, it's setting the
instance variable to nil.

--
Rick DeNatale

Blog: http://talklikeaduck.denhaven2.com/
Twitter: http://twitter.com/RickDeNatale
WWR: http://www.workingwithrails.com/person/9021-rick-denatale
LinkedIn: http://www.linkedin.com/in/rickdenatale
Robert K. (Guest)
on 2009-04-27 10:16
(Received via mailing list)
On 26.04.2009 21:55, Rick DeNatale wrote:
>> You might rather want to use class_eval here.
>
> No instance_eval works just as well for sending attr_accessor to a
> class/module. The only difference with class_eval is when using def to
> define a method.

I did not want to state that instance_eval is the issue.  Sorry for
being imprecise.  A simple

BatchExternalBooking.send "attr_accessor", "message_thread_#$1"

would be sufficient.

>> Also, as Robert pointed out already, using $1 only once and storing the
>> value in a local variable is safer because it can be changed by any method
>> you invoke.
>>
>>>      self.send("message_thread_#{$1}=", $2)
>
> That's not true either. The $n variables are frame local, in a given
> invocation frame they will only change when another regex match is
> done in the same invocation.

A simple test verifies this to be true.  But now I wonder how I have
come to this misconception.  I am pretty sure I stored $1 in a local
variable to avoid issues with changing values.  Maybe I just had another
match in the same method and extended the overwriting problem to method
calls.

Thank you for the education, Rick!

Kind regards

  robert
Brian C. (Guest)
on 2009-04-27 12:20
Tim C. wrote:
> The reason that I am trying to use methods/attributes to store the
> information rather than an array is because this model is used to create
> a batch update form in a Rails project.  If one of the updates fails
> then I want to display the errors on the form next to the relevant
> fields.  For this to work, I will need to use the error_messages_for
> helper to which I need to pass the attribute.
>
> Am I approaching this in the correct manner?

I believe that ostruct by itself will do the job, or you can use
method_missing to delegate to a hash without actually defining any
methods.

At least, I know this works for form helpers. You'd have to test it with
validations and error_messages_for.
David M. (Guest)
on 2009-04-28 01:45
(Received via mailing list)
On Sunday 26 April 2009 12:40:05 Tim C. wrote:
>       BatchExternalBooking.instance_eval "attr_accessor

Why the string eval?

BatchExternalBooking.send :attr_accessor, :"message_thread_#{$1}"

Sorry, I'm a pedant about things like that... Also, you might want to
check
that this does the right thing when the getter is called before the
setter. At
least, your method_missing regex seems to assume that this might be the
case,
but your code doesn't.
Robert D. (Guest)
on 2009-04-28 18:20
(Received via mailing list)
On Mon, Apr 27, 2009 at 11:44 PM, David M. 
<removed_email_address@domain.invalid>
wrote:
> On Sunday 26 April 2009 12:40:05 Tim C. wrote:
>>       BatchExternalBooking.instance_eval "attr_accessor
>
> Why the string eval?
>
> BatchExternalBooking.send :attr_accessor, :"message_thread_#{$1}"
>
> Sorry, I'm a pedant about things like that... Also, you might want to check
Well apart that Robert has said this already nothing to be sorry about.
;)
This is a very concise implementation and I prefer it to mine.
Cheers
Robert
This topic is locked and can not be replied to.