24

I would like to do some fairly heavy-duty reflection in Ruby. I want to create a function that returns the names of the arguments of various calling functions higher up the call stack (just one higher would be enough but why stop there?). I could use Kernel.caller, go to the file and parse the argument list but that would be ugly and unreliable.

The function that I would like would work in the following way:

module A def method1( tuti, fruity) foo end def method2(bim, bam, boom) foo end def foo print caller_args[1].join(",") #the "1" mean one step up the call stack end end A.method1 #prints "tuti,fruity" A.method2 #prints "bim, bam, boom" 

I would not mind using ParseTree or some similar tool for this task but looking at Parsetree, it is not obvious how to use it for this purpose. Creating a C extension like this is another possibility but it would be nice if someone had already done it for me.

I can see that I'll probably need some kind of C extension. I suppose that means my question is what combination of C extension would work most easily. I don't think caller+ParseTree would be enough by themselves.

As far as why I would like to do this goes, rather than saying "automatic debugging", perhaps I should say that I would like to use this functionality to do automatic checking of the calling and return conditions of functions:

def add x, y check_positive return x + y end 

Where check_positive would throw an exception if x and y weren't positive. Obviously, there would be more to it than that but hopefully this gives enough motivation.

1
  • It looks like you want to determine execution order and arguments. Is there a reason you can't use logging? You could probably even do some metaprogramming to open up all the methods in the class and automatically log the arguments to do what you want. Commented Mar 7, 2009 at 22:38

5 Answers 5

41

In Ruby 1.9.2, you can trivially get the parameter list of any Proc (and thus of course also of any Method or UnboundMethod) with Proc#parameters:

A.instance_method(:method1).parameters # => [[:req, :tuti], [:req, :fruity]] 

The format is an array of pairs of symbols: type (required, optional, rest, block) and name.

For the format you want, try

A.instance_method(:method1).parameters.map(&:last).map(&:to_s) # => ['tuti', 'fruity'] 

Of course, that still doesn't give you access to the caller, though.

Sign up to request clarification or add additional context in comments.

4 Comments

This also doesn't return default values. For example: def person(name: "calvin"). method.parameters will give [[:key, :name]].
Default values are evaluated dynamically, you cannot get static information about them. What should def foo(par = rand()); method(:foo).parameters return?
what does instance_method mean here?
@fraxture A bit late, but a method of the class A's instance. Equivalent of doing: A.new.methods rather than A.methods
15

I suggest you take a look at Merb's action-args library.

require 'rubygems' require 'merb' include GetArgs def foo(bar, zed=42) end method(:foo).get_args # => [[[:bar], [:zed, 42]], [:zed]] 

If you don't want to depend on Merb, you can choose and pick the best parts from the source code in github.

Comments

4

I have a method that is quite expensive and only almost works.

 $shadow_stack = [] set_trace_func( lambda { |event, file, line, id, binding, classname| if event == "call" $shadow_stack.push( eval("local_variables", binding) ) elsif event == "return" $shadow_stack.pop end } ) def method1( tuti, fruity ) foo end def method2(bim, bam, boom) foo x = 10 y = 3 end def foo puts $shadow_stack[-2].join(", ") end method1(1,2) method2(3,4,4) 

Outputs

 tuti, fruity bim, bam, boom, x, y 

I'm curious as to why you'd want such functionality in such a generalized manner.

I'm curious how you think this functionality would allow for automatic debugging? You'd still need to inject calls to your "foo" function. In fact, something based on set_trace_func is more able to be automatic, as you don't need to touch existing code. Indeed this is how debug.rb is implemented, in terms of set_trace_func.

The solutions to your precise question are indeed basically, as you outlined. use caller + parsetree, or open the file and grab the data that way. There is no reflection capability that I am aware of that will let you get the names of arguments. You can approve upon my solution by grabbing the associated method object and calling #arity to then infer what of local_variables are arguments, but though it appears the result of that function is ordered, I'm not sure it is safe to rely on that. If you don't mind me asking, once you have the data and the interface you describe, what are you going to do with it? Automatic debugging was not what initially came to mind when I imagined uses for this functionality, although perhaps it is failing of imagination on my part.


Aha!

I would approach this differently then. There are several ruby libraries for doing design by contract already, including ruby-contract, rdbc, etc.

Another option is to write something like:

 def positive lambda { |x| x >= 0 } end def any lambda { |x| true } end class Module def define_checked_method(name, *checkers, &body) define_method(name) do |*args| unless checkers.zip(args).all? { |check, arg| check[arg] } raise "bad argument" end body.call(*args) end end end class A define_checked_method(:add, positive, any) do |x, y| x + y end end a = A.new p a.add(3, 2) p a.add(3, -1) p a.add(-4, 2) 

Outputs

 5 2 checked_rb.rb:13:in `add': bad argument (RuntimeError) from checked_rb.rb:29 

Of course this can be made much more sophisticated, and indeed that's some of what the libraries I mentioned provided, but perhaps this is a way to get you where you want to go without necessarily taking the path you planned to use to get there?

2 Comments

In fact, the method you describe clearly fails to distinguish arguments from local variables while also failing to work automatically
Well, that works for doing design by contract but it seems more than a little klunky compared to just having a single function you could call. The ideal is to be able easily add and remove the checks.
1

if you want the value for the default values, too, there's the "arguments" gem

$ gem install rdp-arguments $ irb >> require 'arguments' >> require 'test.rb' # class A is defined here >> Arguments.names(A, :go) 

Comments

-4

In fact, the method you describe clearly fails to distinguish arguments from local variables while also failing to work automatically

That's because what you're trying to do is not something which is supported. It's possible (everything is possible in ruby), but there's no documented or known way to do it.

Either you can eval the backtrace like what logan suggested, or you can bust out your C compiler and hack sourcecode for ruby. I'm reasonably confident there aren't any other ways to do this.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.