Ruby metaprogramming and own custom attr_accessor

March 3, 2012 · Posted in Development 

Ruby is a dynamic language, that’s why you won’t get compile time type warnings/errors as you get in languages like C#.

Ruby has a lot of metaprogramming features allowing you to create your own custom methods on the fly which can be used the same as att_accessor.

dynamic method definition – define_method

Use the following code to define a new method in your class on the fly:

class Myclass
define_method("your_method_name_here") do
# your code here
end
end

 

Custom attr_accessor method accessible from all classes

To create a method that will be accesible from all classes you need to add code to the Class class. In Ruby a class is simply an object of class Class. Ruby provides a method class_eval that takes a string and evaluates it in the context of the current class, that is, the class from which you’re calling your attr_accessor.

class Class
def my_custom_attr_method(attr_name)
# your code here
end
end
# you can use this method in any of your classes
class Myclass
my_custom_attr_method :attr1
end

 

attr_accessor

Ruby’s method attr_accessor uses metaprogramming to create getters and setters for object attributes on the fly.

It creates the code like this:

class Class
 
def my_attr_accessor(*args)
 
    # iterate through each passed in argument...
    args.each do |arg|
 
        # getter
        self.class_eval("def #{arg};@#{arg};end")
 
        # setter
        self.class_eval("def #{arg}=(val);@#{arg}=val;end")
 
    end
 
end
end

 

Examples

Below is some examples of using metaprogramming.

 

Create custom attr_accessor to track the history of values

Define a method attr_accessor_with_history that provides the same functionality as attr accessor but also tracks every value the attribute has ever had.

This method was taken from the home work task of the class ‘Software as a service’ – https://www.coursera.org/saas/auth/welcome.

 

class Class
 
  def attr_accessor_with_history(attr_name)
    attr_name = attr_name.to_s
    attr_hist_name = attr_name+'_history'
 
    #getter
    self.class_eval("def #{attr_name};@#{attr_name};end")
 
    #setter
    self.class_eval %Q{
      def #{attr_name}=(val)
        # add to history
        @#{attr_hist_name} = [nil] if @#{attr_hist_name}.nil?
        @#{attr_hist_name} << val
 
        # set the value itself
        @#{attr_name}=val
      end
 
      def #{attr_hist_name};@#{attr_hist_name};end
 
                    }
 
  end
 
end

 

Usage of this accessor:

class Foo
attr_accessor_with_history :bar
def initialize(some_bar_value)
self.bar = some_bar_value
end
end
 
f = Foo.new
f.bar = 3
f.bar = :wowzo
f.bar = 'boo!'
puts f.bar_history # => [nil, 3, :wowzo, 'boo!']

 

Note! You should use self.bar in the initializer method so it calls our custom setter to track the history.

If you write bar=some_bar_value then it will create a local variable ‘bar’ that doesn’t do what is intended.

 

Strongly typed attr_accessor

class Myclass
  def self.typesafe_accessor(name, type)
 
  define_method(name) do
    instance_variable_get("@#{name}")
  end
 
  define_method("#{name}=") do |value|
    if value.is_a? type
      instance_variable_set("@#{name}", value)
    else
      raise ArgumentError.new("Invalid Type")
    end
  end
end
 
typesafe_accessor :foo, Integer
 
end
 
# usage
 
f = Foo.new
f.foo = 1
f.foo = "bar" # an exception thrown here!

You can modify this code to create an accessor so it can be used in all classes. To do this – move this code to class Class and modify it a little like in the first example.

 

This code is based on this explanation: http://stackoverflow.com/questions/7988410/attr-accessor-strongly-typed-ruby-on-rails

 

Read more:

- Meta-programming in Ruby – http://learnbysoft.blogspot.com/2010/10/meta-programming-in-ruby-part-1-alias.html

- Screencasts on The Ruby Object Model and Metaprogramming – http://pragprog.com/screencasts/v-dtrubyom/the-ruby-object-model-and-metaprogramming

Comments

  • Josephquigley

    Thanks very much for these examples. Regarding attr_accessor_with_history. What if the class were set up like this?

    class Foo
    attr_accessor_with_history :bar

    def initialize(value)
    @bar = value
    end

    end
    f = Foo.new(4)
    f.bar = 3 # => 3
    f.bar = :wowzo
    p f.bar_history

    #we lose the initial value of bar which was set to 4
    #Thoughts? I would think we would want to capture the initial value of bar => ’4′

    #Thanks!

  • Anonymous

    anytime you want to change @bar’s value you need to use its setter, but do not modify @bar directly. Otherwise it breaks the logic.

    So instead of this

    def initialize(value)
    @bar = value
    end

    write this:

    def initialize(value)
    self.bar= value
    end

    it will call the setter that tracks a history.

  • Josephquigley

    Yes, thanks very much. That seems like a nice strategy, but I was looking for a way to leave the standard ruby initialize syntax in place, but still track values. I just haven’t been able to think of a way that works.

  • Anonymous

    I see these points
    - The class which has this instance variable must have the ability to change this variable directly.
    It must be some way to change the value directly. This way is @varname = value.
    - In order to do something more while changing the value of variable – that’s what methods like getters/setters are about.

    - To be 100% sure you don’t change the value of variable without changing the history then I would think of this OOP technique:
    1. define a super (parent) class whose job is
    define an instance variable as private, define getter/setter to track the history
    2. in a derived class:
    access the variable only by getters/setters.

    BUT! It is Ruby..
    Ruby doesn’t provide visibility control over variable. Ruby allows too much to do with classes like reopening classes.. So it is your responsibility how you use Ruby’s cool stuff.

    We can think what we want to see new to Ruby.
    But I don’t think it would be a good way for Ruby to add some metaprogramming feature to modify the behaviour of this assignment operator: @var = value. I feel that Ruby introduced @-sign intentionally and specially for accessing the variable directly.

  • orlandodelaguila

    i was doing this homework too.. but i dont see why theres the need to put nil inside the array in

    @#{attr_hist_name} = [nil] if @#{attr_hist_name}.nil?

    i know its a requirement…. but why?

    and in that piece of code you could use something like this

    @#{attr_hist_name} ||= [nil]

    that will set attr_hist_name to [nil] only if attr_hist_name.nil?.. much more like the if you have but a lil more clean..

  • Anonymous

    @#{attr_hist_name} ||= [nil] – that’s more Ruby way for initializing a variable. thank you.

    I can’t think of any reason in initialization of the history with [nil] instead of just empty array []. I was asking the same question. Why?

  • Sarah H

    I’m struggling to get this code to work for me. I tried to integrate the ideas into my code but couldn’t get it to work. Finally I just copied everything you have and put it into my .rb file but I keep getting the Invalid number of arguments for the initialize method. Any ideas?

  • Anonymous

    paste your code somewhere to see it.
    For example, use http://pastebin.com/

    Try to use it without an initializer:

    class Foo
    attr_accessor_with_history :bar
    end

    f = Foo.new
    f.bar = 3
    f.bar = :wowzo
    f.bar = ‘boo!’
    puts f.bar_history # => [nil, 3, :wowzo, 'boo!']

  • Randy Fay

    Since this same homework (attr_accessor_with_history) is still in use for that class, you probably shouldn’t give the literal answer to that homework exercise.