Friday, January 1, 2010

Ruby and Simple Dynamic Programming III

In my last couple of posts, here and here, I began talking about dynamic programming using Ruby. In this post, I'm going to continue on the same lines and this time we're going to discuss using the eval series of methods, specifically class_eval. Before getting started however, here's a word of warning. Gregory Brown in Ruby Best Practices describes eval (and related methods) as "evil". The truth is that you need to be pretty careful about using these methods as they offer ample opportunity for a user to do some pretty bad things to your system. With that said ...

As you know, Ruby offers three convenience methods for read, write, and read/write access to variables and these are attr_reader, attr_writer, and attr_accessor respectively. Let's take a look at how we could implement these if they hadn't already been thoughtfully provided.

Here's the code:

# Open the class Class and add three new access methods, access_r, access_w, access_rw.
# These are really just reimplementations for attr_reader, attr_writer, attr_accessor.
class Class
# Provide read access only. Take a list of symbols and create a
# method that will all the user to access each symbol.
def access_r(*symbols)
symbols.each { | symbol |
class_eval "def #{symbol}() @#{symbol}; end"
}
end

# Provide write access only. Take a list of symbols and create a
# method that will all the user to write each symbol.
def access_w(*symbols)
symbols.each { | symbol |
class_eval "def #{symbol}=(val) @#{symbol} = val; end"
}
end

# Provide read/write access. Take a list of symbols and create two
# methods that will all the user to read and write each symbol.
def access_rw(*symbols)
symbols.each { | symbol |
class_eval "def #{symbol}() @#{symbol}; end"
class_eval "def #{symbol}=(val) @#{symbol} = val; end"
}
end

end

if __FILE__ == $PROGRAM_NAME

# Using the new attribute accessor methods.
class Foo
access_r :bar
access_w :qux
access_rw :baz, :quux

# Since bar is read only from the outside, we'll just initialize
# it here.
def initialize(bar)
@bar = bar
end

# Since qux is write only, we'll create a method that lets us view
# it.
def show_qux
puts "qux = #{@qux}"
end

end


# Create a new foo and initialize bar (read only) with goodbye.
# Show that we can then access it.
foo = Foo.new ("goodbye")
puts "foo.bar = #{foo.bar}"


# Set and then access the two variables we set to
# read/write.
foo.baz = "hello"
puts "foo.baz = #{foo.baz}"

foo.quux = "world"
puts "foo.quux = #{foo.quux}"

# Set the write only field and then display it using the show_qux
# method.
foo.qux = "test"
foo.show_qux

# Try to access the write only field for reading. This should
# fail with an undefined method.
puts foo.qux

end



We start out opening Class as this is where we're going to add our new methods, access_r, access_w, and access_rw. We're going to take a list of symbols, represented here with "*symbols" and covert each of the symbols into a new method. For access_r, we'll loop through the symbols and then generate a method with the name "symbol" that returns symbol. For example if we have a symbol :x, we'll get a method that looks like def x() @x; end. The access_rw will give us, for the same symbol :x, def x=(val) @x=val; end. Finally, the access_rw will give us both methods (there's probably a clean way to refactor this so we don't have duplicated code, but this is left as an exercise for the reader).

Finally, we create a class Foo, that uses all three access methods and then some code that exercises each one. The final call is a read access to a write only field and should fail with an undefined method.

As always, let me know if you have any questions or comments.

4 comments:

  1. But you can do that without eval. Write a class method using define_method() and instance_variable_get() / instance_variable_set().

    The point I was trying to make in the book is that not only is eval() unsafe, it's not necessary for most of the things people use it for.

    ReplyDelete
  2. Gregory, First off, let me thank you for writing. I always appreciate that. Second, I really did get that out of your book (which is excellent by the way) and I should have noted this above. In this case though, I'm not sure that using eval is all that bad, but, as I've done in the past when people have made good suggestions, I'll take a look at redoing the post with the define_method/instance_variable_get/set methods.

    Once again, thanks for your thoughts!

    ReplyDelete
  3. Yeah, here it isn't really dangerous, just unnecessary.

    ReplyDelete
  4. Whoops, forgot to also mention that you'll get much better debugging info without eval here.

    ReplyDelete