I'm able to get access to a Ruby method's arguments using the TracePoint API:
def foo(foo_arg)
end
trace = TracePoint.trace(:call, :c_call) do |tp|
tp.disable
case tp.method_id
when :foo, :sub
method = eval("method(:#{tp.method_id})", tp.binding)
method.parameters.each do |p|
puts "#{p.last}: #{tp.binding.local_variable_get(p.last)}"
end
end
tp.enable
end
trace.enable
foo(10)
# => foo_arg: 10
However when I try this with a c method call, I get an error.
"foo".sub(/(f)/) { $1.upcase }
script.rb:20:in `method': undefined method `sub' for class `Object' (NameError)
from script.rb:20:in `<main>'
from script.rb:8:in `eval'
from script.rb:8:in `block in <main>'
from script.rb:20:in `<main>'
This looks like it happens because of a discrepancy between the binding returned when using a C method call and regular Ruby method call.
In the Ruby case tp.self is equal to tp.binding.eval("self") is main however in the C case tp.self is "foo" and tp.binding.eval("self") is main. Is there a way to get the arguments passed into a method using TracePoint for both Ruby and C defined methods?
As you point in your question and as it documented in ruby documentation, tp.self returns a traced object, which have a method method you are looking for.
I think you should use
method = tp.self.method(tp.method_id)
instead of
method = eval("method(:#{tp.method_id})", tp.binding)
UPDATE. Some explanation regarding your last paragraph in question. tp.self in first case (when you call foo) is point to main, because you define foo method in main context and it points to String object in second case because sub is defined there. But tp.binding.eval("self") returns main in both cases because it returns a calling context (not a 'define' context as you expect) and in both cases it is main.
UPDATE (in reply to comment) I think that the only way to do this is to monkey patch sub and all other methods that you are interesting for. Code example:
class String
alias_method :old_sub, :sub
def sub(*args, &block)
old_sub(*args, &block)
end
end
trace = TracePoint.trace(:call, :c_call) do |tp|
tp.disable
case tp.method_id
when :sub
method = tp.self.method(tp.method_id)
puts method.parameters.inspect
end
tp.enable
end
trace.enable
"foo".sub(/(f)/) { |s| s.upcase }
One big drawback is that you can't use $1, $2, ... vars in your original blocks. As pointed here where is no way to make it works. However you can still use block parameters (s in my example).
Related
When I pass a lambda to instance_eval as the block, it seems to pass an extra argument:
lamb = -> { puts 'hi' }
proc = Proc.new { puts 'hi' }
instance_eval(&lamb)
# >> ArgumentError: wrong number of arguments (given 1, expected 0)
# from (irb):5:in `block in irb_binding'
# from (irb):7:in `instance_eval'
instance_eval(&proc)
# => hi
instance_exec(&lamb)
# => hi
Why is this the case? Note that this question is NOT about why lambda throws an error. That is well understood. The question is about WHY instance_eval sends self of the receiver as a parameter. It is not needed, and confusing. And AFAIK not documented.
This helps, but doesn't explain WHY ruby would do it this way. The whole point of instance_eval is to set self to the receiver; why confuse things by also passing self to the proc?
From the docs
For procs created using lambda or ->() an error is generated if the
wrong number of parameters are passed to a Proc with multiple
parameters. For procs created using Proc.new or Kernel.proc, extra
parameters are silently discarded.
In your case both lamb and proc called with one parameter
From the docs of instance_eval
When instance_eval is given a block, obj is also passed in as the
block's only argument
instance_eval is method of BasicObject class and can be called within instance. So given block will have access for private methods for example.
class Test
def call
secret_number + 100
end
private
def secret_number
42
end
end
test = Test.new
show_secret = -> (obj) { puts secret_number }
test.instance_eval(&show_secret) # print 42
Without instance self of current context will be passed as an argument. I think instance_eval was designed more for calling it within objects.
From the docs of instance_eval
In order to set the context, the variable self is set to obj while the
code is executing, giving the code access to obj's instance variables
and private methods.
Given this script
def hash
puts "why?"
end
x = {}
x[[1,2]] = 42
It outputs the following
why?
/tmp/a.rb:6:in `hash': no implicit conversion of nil into Integer (TypeError)
from /tmp/a.rb:6:in `<main>'
It seems that the hash function defned in the script is overriding Array#hash in that case. Since the return value of my hash method is nil and not an Integer, it throws an exception. The following script seems to confirm this
puts [1,2,3].hash
def hash
puts "why?"
end
puts [1,2,3].hash
The output is
-4165381473644269435
why?
/tmp/b.rb:6:in `hash': no implicit conversion of nil into Integer (TypeError)
from /tmp/b.rb:6:in `<main>'
I tried looking into the Ruby source code but could not figure out why this happens. Is this behavior documented?
You're not overriding Array#hash, you're shadowing Kernel#hash by creating Object#hash:
puts method(:hash)
def hash
puts "why?"
end
puts method(:hash)
That prints:
#<Method: Object(Kernel)#hash>
#<Method: Object#hash>
Fix it so we can see more:
def hash
puts "why?"
super
end
x = {}
x[[1,2]] = 42
Now the output is:
why?
why?
And no error. Try it with x[[1,2,3,4,5,6,7]] = 42 and you'll instead see why? printed seven times. Once for each array element, since the array's hash method uses the hashes of its elements. And Integer#hash doesn't exist, it inherits its hash method from Object/Kernel, so yours gets used.
This is due to a kind of hack in Ruby top level. Have you ever wondered how this works?
def foo
end
p self
foo
class Bar
def test
p self
foo
end
end
Bar.new.test # no error
How are two totally different objects (main and a Bar) able to call foo like it's a private method call? The reason is because... it is a private method call.
When you define a method at the top level of your Ruby script, it gets included (via Object) in every object. That's why you can call top-level methods like they are global functions.
But why does this break only hash and not other common methods? def to_s;end won't break to_s, for example. The reason is because hash is recursive: most* class implementations ultimately call down to Object#hash for their implementations. By redefining that base case, you break it globally. For other methods like to_s you won't see a global change because it's way up the inheritance chain and doesn't get invoked.
* the only objects this doesn't break are a few literals that probably have hard-coded hash values e.g. [] {} "" true etc.
I'm doing this and it works:
class B
def value
"X"
end
end
class A
def initialize(context)
#context = context
end
def m
Proc.new do
value
end
end
def execute
#context.instance_eval(&m)
end
end
A.new(B.new).execute #=> "X"
But calling m.to_proc is not working...
class B
def value
"X"
end
end
class A
def initialize(context)
#context = context
end
def m
value
end
def execute
#context.instance_eval(&m.to_proc)
end
end
A.new(B.new).execute #=> NameError: undefined local variable or method `value' for #<A:0x007fae2ab02040 #context=#<B:0x007fae2ab02108>>
I want to know why these two examples are different and how to make it work with to_proc
In the second snippet, you are calling m, which returns the result of calling value, which is undefined. (And even if if were somehow magically calling B#value, then B#value returns a String and Strings don't respond to to_proc, so you would get a NoMethodError there.) In the first snippet, you call m, which returns a Proc.
It looks like you are trying to pass the method m itself instead of the result of calling it. In Ruby, methods aren't objects, so you can't just grab them and pass them around (and even if methods were objects, then m is still the syntax for calling m, not for referencing it). You have to ask Ruby's reflection API for a reflective proxy for the method first, using the Object#method method, which returns a Method object representing the method:
#context.instance_eval(&method(:m).to_proc)
Note that the call to to_proc is completely redundant here, since & will call to_proc anyway if the argument isn't a Proc already. (You may have seen something like foo.map(&:bar) before, which will invoke Symbol#to_proc.)
#context.instance_eval(&method(:m))
Is there a way to specify a class method such that when the object is used as if it were a function, that method is called? Something like this:
class MyClass
def some_magic_method(*args)
# stuff happens
end
end
# create object
myob = MyClass.new
# implicitly call some_magic_method
myob 'x'
You could write a command class and make use of a ruby shortcut
class MyClass
def self.call(text)
puts text
end
end
MyClass.('x')
Here MyClass.() defaults to the call class method.
As mentioned by #CarySwoveland in the comments you can use method_missing. A basic example is as follows:
class MyClass
def method_missing(method_name, *args)
if method_name.match?(/[xyz]/)
send(:magic_method, args.first)
else
super
end
end
def magic_method(a)
a = 'none' if a.nil?
"xyz-magic method; argument(s): #{a}"
end
end
myob = MyClass.new
myob.x #=> "xyz-magic method; argument(s): none"
myob.x(1) #=> "xyz-magic method; argument(s): 1"
myob.y #=> "xyz-magic method; argument(s): none"
myob.z #=> "xyz-magic method; argument(s): none"
This captures all methods named x, y or z. Our else branch sends all other undefined methods to the original method_missing:
myob.v #=> test.rb:7:in `method_missing': undefined method `v' for
#<MyClass:0x000000021914f8> (NoMethodError)
#from test.rb:25:in `<main>'
What methods you capture is up to you and is determined by the regex /[xyz]/ in this case.
Key methods: BasicObject#method_missing, Object#send. For further info check out this question, read Eloquent Ruby by Russ Olsen (from which this answer references)
You meant to invoke some class' instance method when the object is invoked as a function. This is already supported: instance method call gets called when you "invoke" an object via the functional invocation method () (for more details, see here How do I reference a function in Ruby?).
class C
def call(x)
puts "Called with #{x}"
end
end
obj = C.new
obj.(88) # Called with 88 => nil
obj (88) # NoMethodError: undefined method `obj' for main:Object
If you do want the latter syntax, a horrible trick is the following one (but works only at the top-level, unless you carry along the bindings):
module Kernel
def method_missing(name,*args)
obj = begin
TOPLEVEL_BINDING.local_variable_get(name)
rescue
nil
end
return super if obj.nil?
obj.send :call, *args
end
end
obj = C.new
obj 88 # Called with OK => nil
This example also wants to communicate that you should always keep in mind
who is the receiver of your method calls, and what syntaxes are available for calling methods (especially when you leave out dots and parentheses).
class D
def obj; C.new end
def f
#(obj) 88 # BAD
(obj).(88)
#obj() 88 # BAD
obj().(88)
end
end
The point is that you do not actually have functions, but methods that get called on objects. If you omit the receiver of a method call, the receiver defaults to self, the current object. But in your example, myob does not appear as an explicit receiver (since there is not following dot as in myob.), hence the current object is looked for a method myob.
I'm trying to alias a method that use's Ruby's special $& (returns last regex match). I can do this manually and it works:
original = String.instance_method(:sub)
String.send(:define_method, :sub) do |*args, &block|
puts "called"
original.bind(self).call(*args, &block)
end
"foo".sub(/f/) { $&.upcase }
called
# => "Foo"
However if I try to write a method that does this for me, it fails:
def programatic_alias(klass, method_name)
original = klass.instance_method(method_name)
klass.send(:define_method, method_name) do |*args, &block|
puts "called"
original.bind(self).call(*args, &block)
end
end
programatic_alias(String, :sub)
"foo".sub(/f/) { $&.upcase }
called
NoMethodError: undefined method `upcase' for nil:NilClass
called
called
called
from (irb):19:in `block in irb_binding'
It looks like the global state is being affected by the scope of the programatic_alias method, but I'm not sure if that's what's going on. The questions is this: how can I programmatically alias String#sub so that it still works with Ruby's special global variables?
As far as I know, you can't do this. The docs say
These global variables are thread-local and method-local variables.
If you dig into the ruby source, accessing $& calls last_match_getter which gets its data from rb_backref_get, which calls vm_svar_get which (skipping over a few more internal methods) gets the current control frame and reads the data from there. None of this data is exposed to the ruby api - there's no way to propagate this data from one frame to the one you want to access it in.
In your second example the call to the original method is happening inside your programatic_alias method, and so $& is being set in that scope. For the same reason
'foo'.try(:sub, /f/) {$&.upcase}
won't work either.
Your first example half works because the place where sub is called and the place where $& is referenced (inside the block) are in the same method scope (in this case the ruby top level). Change it to:
original = String.instance_method(:sub)
String.send(:define_method, :sub) do |*args, &block|
puts "called"
original.bind(self).call(*args, &block)
end
def x
"foo".sub(/f/) { $&.upcase }
end
x()
and $& is no longer defined in your block (if you catch the exception thrown by x you can see that $& is being set at the top level)