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:
Use define_method:
@arr.each do |method|
self.class.class_eval do
define_method method do |*arguments|
puts arguments
end
end
endUse 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
Rmagick Installation: Can't Find Magickwand.H
Method_Missing Gotchas in Ruby
Ruby Working on Array Elements in Groups of Four
Where to Place/Access Config File in Gem
How to Check to See If a File Exists (On the Remote Server) in Capistrano
How to Create a Delete Link for a Related Object in Ruby on Rails
Rails: Money Gem Converts All Amounts to Zero
How to Ban/Block Users with Devise for Rails
Parsing Atom & Rss in Ruby/Rails
Why Does This Nokogiri Xpath Have a Null Return
How to Step Out of a Loop with Ruby Pry
How to Upgrade to the Current Version of Ruby (2.2.3) on Os X V10.6.8
When to Use Struct Instead of Hash in Ruby
Imagemagick 7 with Rmagick 2.16 on MACos Sierra Can't Find Magickwand.H