Ruby - Using Class_Eval to Define Methods

Ruby - Using class_eval to define methods

This was fun!!!

class Class
def attr_accessor_with_history(attr_name)
attr_name = attr_name.to_s # make sure it's a string
attr_reader attr_name
attr_reader attr_name+"_history"
class_eval %Q"
def #{attr_name}=(value)
if !defined? @#{attr_name}_history
@#{attr_name}_history = [@#{attr_name}]
end
@#{attr_name} = value
@#{attr_name}_history << value
end
"
end
end

class Foo
attr_accessor_with_history :bar
end

class Foo2
attr_accessor_with_history :bar
def initialize()
@bar = 'init'
end
end

f = Foo.new
f.bar = 1
f.bar = nil
f.bar = '2'
f.bar = [1,nil,'2',:three]
f.bar = :three
puts "First bar:", f.bar.inspect, f.bar_history.inspect
puts "Correct?", f.bar_history == [f.class.new.bar, 1, nil, '2', [1,nil,'2',:three], :three] ? "yes" : "no"
old_bar_history = f.bar_history.inspect

f2 = Foo2.new
f2.bar = 'baz'
f2.bar = f2
puts "\nSecond bar:", f2.bar.inspect, f2.bar_history.inspect
puts "Correct?", f2.bar_history == [f2.class.new.bar, 'baz', f2] ? "yes" : "no"

puts "\nIs the old f.bar intact?", f.bar_history.inspect == old_bar_history ? "yes" : "no"

Note that the only reason you need to use strings with class_eval is so that you can refer to the value of attr_name when defining the custom setter. Otherwise one would normally pass a block to class_eval.

Creating methods with class_eval

This is because class_eval is a class method and you're calling it in the context of an instance. You can do this instead:

class Class
def createMethod(attr_name)
attr_name = attr_name.to_s
self.class.class_eval %Q{
def #{attr_name}
puts "bar"
end
}
self # Return yourself if you want to allow chaining methods
end
end

Here's the output from irb when doing this:

irb(main):001:0> class Class
irb(main):002:1> def createMethod(attr_name)
irb(main):003:2> attr_name = attr_name.to_s
irb(main):004:2> self.class.class_eval %Q{
irb(main):005:2" def #{attr_name}
irb(main):006:2" puts "bar"
irb(main):007:2" end
irb(main):008:2" }
irb(main):009:2> end
irb(main):010:1> end
=> nil
irb(main):011:0> clazz = Class.new
=> #<Class:0x007fd86495cd58>
irb(main):012:0> clazz.respond_to?(:foo)
=> false
irb(main):013:0> clazz.createMethod("foo")
=> nil
irb(main):014:0> clazz.respond_to?(:foo)
=> true

How to define a class method :[] when using class_eval

The method's name is [], but it still takes its argument list in the usual way

def self.[](ary)
...
end

Then you call it as Quaternion[ary]

Define instance method of a class after class already defined in ruby

If I understand you correctly, you need to be able to access the commentable variable inside your Thread extension, right?

If so, just change this:

Thread.class_eval do

To this:

Thread.class_exec(commentable) do |commentable|

And it should work.

Ruby class_eval method

There are two accepted ways:

  1. Use define_method:

    @arr.each do |method|
    self.class.class_eval do
    define_method method do |*arguments|
    puts arguments
    end
    end
    end
  2. Use class_eval with a string argument:

    @arr.each do |method|
    self.class.class_eval <<-EVAL
    def #{method}(*arguments)
    puts arguments
    end
    EVAL
    end

The first option converts a closure to a method, the second option evaluates a string (heredoc) and uses regular method binding. The second option has a very slight performance advantage when invoking the methods. The first option is (arguably) a little more readable.

How do I use class_eval?

The short answer is: you probably want to avoid using class_eval like this.

Here's an explanation of your code:

The %{hello} is just another way to write a string literal in Ruby, without having to worry about escaping double or single quotes within the string:

%{hello "world"} == "hello \"world\""  # => true

The val in your code is an argument of the method being defined.

The class_eval is used to define some methods by computing the text one would write to do the definition and then evaluating it. It is not necessary here, BTW. An equivalent code would be:

class Module
def attr_ (*syms)
syms.each do |sym|
define_method "#{sym}=" do |val|
instance_variable_set "@#{sym}", val
end
end
end
end

This is just equivalent to the builtin attr_writer.

Update: There can actually be a significant difference between the two...

The class_eval version is vulnerable if you can't trust the argument syms. For example:

class Foo
attr_ "x; end; puts 'I can execute anything here!'; val=42; begin; val"
end

The class_eval version will print "I can execute anything here" twice, proving it can execute anything. The define_method version won't print anything.

This type of code was pivotal to create major vulnerability for all installed Rails apps.

Using send method inside define_method in class_eval block

The reciever is the current value of self

So you can do:

class MethodLogger
def log_method((klass,method_name)
klass.class_eval do
alias_method "#{method_name}_original" method_name
define_method method_name do
puts "#{Time.now}: Called #{method_name} on #{self.class}"
send "#{method_name}_original"
end
end
end
end

After a while you'll learn to keep track of self in your mind :)

Ruby / Rails meta programing, how to define instance and class methods dynamically?

The key here is to use class_eval to open up the class you are calling denormalizable_collection on.

A simplified example is:

class Foo
def self.make_method(name)
class_eval do |klass|
klass.define_singleton_method(name) do
name
end
end
end

make_method(:hello)
end

irb(main):043:0> Foo.hello
=> :hello

module DenormalizableCollection
def self.included(base)
base.extend ClassMethods
end

module ClassMethods
def denormalizable_collection(*actions)
actions.each do |action|
generate_denormalized_methods(action)
generate_instance_methods(action)
generate_callbacks(action)
end
end
private
def generate_denormalized_methods(action)

self.class_eval do |klass|
# you should consider if these should be instance methods instead.
# define News.most_popular
define_singleton_method "#{action}" do
collection = Redis.current.get(send("#{action}_key"))
return [] unless collection.present?
JSON.parse(collection).map { |h| DenormalizedHash.new(h) }
end
# define News.most_popular
# define News.set_most_popular
define_singleton_method "set_#{action}" do
Redis.current.set(send("#{action}_key"), send("#{action}_data").to_json)
end
# define News.most_popular_data, which is a method that returns an array of hashes
define_singleton_method "#{action}_data" do
raise NotImplementedError, "#{action}_data is required"
end
# define News.most_popular_key, the index key to use inside of redis
define_singleton_method "#{action}_key" do
"#{name.underscore}_#{action}".to_sym
end
end
end

def generate_callbacks(action)
self.class_eval do
# Since callbacks call instance methods you have to pass a
# block if you want to call a class method instead
after_commit -> { self.class.send("set_#{action}") }
after_destroy -> { self.class.send("set_#{action}") }
end
end

def generate_instance_methods(action)
class_eval do
define_method :a_test_method do
# ...
end
end
end
end
end

Note here that I'm not using ActiveSupport::Concern. Its not that I don't like it. But in this case it adds an additional level of metaprogramming thats enough to make my head explode.



Related Topics



Leave a reply



Submit