Ruby metaprogramming and own custom attr_accessor

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:

[codesyntax lang=”ruby”]

class Myclass

define_method(“your_method_name_here”) do
# your code here
end

end

[/codesyntax]

 

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.

[codesyntax lang=”ruby”]
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

[/codesyntax]

 

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:

[codesyntax lang=”ruby”]

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

[/codesyntax]

 

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.

 

[codesyntax lang=”ruby”]

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

[/codesyntax]

 

Usage of this accessor:

[codesyntax lang=”ruby”]
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!’]

[/codesyntax]

 

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

[codesyntax lang=”ruby”]

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!

[/codesyntax]

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

10 thoughts on “Ruby metaprogramming and own custom attr_accessor”

  • Josephquigley says:

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

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

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

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

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

      @#{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?

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

    • 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!’]

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

  • ananiask8 says:

    I’m trying to do the same. Been trying for hours to do something more elegant with history(:bar). Isn’t there a way to do this without having to create those two string variables and just using the fact that we are calling, for example, the history method with the :bar argument? It seems to me that extracting that info, we should be able to do what we want. The problem is that I can’t find how to do that anywhere.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>