1

I'm writing a recursive function. It would be nice if there were a good way to tell if the current run of the method is within the recursion stack. Something like this:

def mymethod
  if yourcodehere
    # do stuff
  end
  
  mymethod()
end

Any ideas? I'll write a ruby gem for it if there's a good way.

tscheingeld
  • 789
  • 7
  • 24
  • I'm not sure what you mean; but could add some logging within the function. A simple `puts` would do the trick. Maybe add a counter which ++ every time the recursive function is run. – Sagar Pandya Jun 27 '23 at 23:21
  • 1
    Does this answer your question? [How to get the name of the calling method?](https://stackoverflow.com/questions/5100299/how-to-get-the-name-of-the-calling-method) – Chris Jun 27 '23 at 23:44
  • Ruby *as a language* is really optimized for iteration rather than recursion. That doesn't mean you *can't* implement recursive calls; just that you're more likely to experience problems related to a lack of TCO, maximum stack depth, or the memory consumed by each stack frame. Unless you specifically need to use an algorithm that requires recursion and are prepared to explicitly rescue [SystemStackError, NoMemoryError, or related exceptions](https://docs.ruby-lang.org/en/3.2/Exception.html), you're usually better off using built-in iterators like #map or memoized methods and procs. YMMV. – Todd A. Jacobs Jun 28 '23 at 18:53

2 Answers2

2

You could utilize caller_locations:

def mymethod(x)
  first, *stack = caller_locations(0)
  recursion = stack.any? do |cl|
    cl.absolute_path == first.absolute_path && cl.label == first.label
  end

  p x: x, recursion: recursion

  mymethod(x + 1) if x < 3
end

mymethod(1)
# {:x=>1, :recursion=>false}
# {:x=>2, :recursion=>true}
# {:x=>3, :recursion=>true}

By passing a start value of 0, caller_locations will return its own invocation which you can compare to the remaining caller locations to detect recursion, i.e. even across multiple method calls. You can limit the number of entries by passing a second limit parameter, e.g. if you only need to detect direct invocations.

Note that the above doesn't work when enabling tail call optimization because it effectively re-uses the last stack frame which makes every recursive call look like the initial one.

A more simplistic approach is to just pass an optional argument when calling the method from itself to explicitly indicate a recursive call, e.g.:

def mymethod(x, recursion: false)
  p x: x, recursion: recursion

  mymethod(x + 1, recursion: true) if x < 3
end

mymethod(1)
# {:x=>1, :recursion=>false}
# {:x=>2, :recursion=>true}
# {:x=>3, :recursion=>true}

Aside from debugging or for detecting (unintentional) infinite loops, I would avoid having a method behave differently based on its calling context.

Stefan
  • 109,145
  • 14
  • 143
  • 218
1

Matching Caller Against Method Name

There's more than one way to do what you want, but it's trivial to determine if the caller is the current method or not. For example, assuming raw performance isn't your goal then the following code works just fine.

def foo
  puts caller.first.match?(__method__.to_s) ? "tail call" : "first call"
  sleep 0.1
  foo
end

This will return:

first call
tail call
tail call
tail call
...

because the ternary condition is only true when Kernel#caller contains the current method identified by Kernel#__method__.

Passing a Boolean Argument to a Tail Call

I'm unsure if there would be a performance penalty to the approach above as your stack gets deeper. If that happens, you might be try passing explicit start and length arguments to Kernel#caller, or just pass a Boolean argument as part of your tail call. For example:

def foo recursion=false
  puts recursion ? "tail call" : "first call"
  sleep 0.1
  foo recursion: true
end

This gives exactly the same results as the first example, but no longer needs to do a regular expression match against the stack trace or introspect the Symbol for the current method. It simply passes a truthy value as a keyword argument when making the tail call, so this approach is likely faster to evaluate. However, I didn't bother to benchmark either approach, so your mileage may vary.

Todd A. Jacobs
  • 81,402
  • 15
  • 141
  • 199