We have a huge codebase of over 700,000 lines of Perl spread across a couple dozen Git repositories at work. Sometimes refactoring is easy if the classes and methods involved are confined to one of those repos, but last week we wanted to rename a method that was potentially used across many of them without having to QA and launch so many changes. After getting some help from Dan Book and Ryan Voots on the #perl libera.chat IRC channel, I arrived at the following solution.
First, if all you want to do is alias the new method call to the old while making the least amount of changes, you can just do this:
*new_method = \&old_method;
This takes advantage of Perl’s typeglobs by assigning to the new method’s name in the symbol table a reference (indicated by the \
character) to the old method. Methods are just subroutines in Perl, and although you don’t need the &
character when calling one, you do need it if you’re passing a subroutine as an argument or creating a reference, as we’re doing above.
I wanted to do a bit more, though. First, I wanted to log the calls to the old method name so that I could track just how widely it’s used and have a head start on renaming it elsewhere in our codebase. Also, I didn’t want to fill our logs with those calls—we have enough noise in there already. And lastly, I wanted future calls to go directly to the new method name without adding another stack frame when using caller
or Carp.
With all that in mind, here’s the result:
sub old_method {
warn 'old_method is deprecated';
no warnings 'redefine';
*old_method = \&new_method;
goto &new_method;
}
sub new_method {
# code from old_method goes here
}
Old (and not-so-old) hands at programming are probably leaping out of their seats right now yelling, “YOU’RE USING GOTO! GOTO IS CONSIDERED HARMFUL!” And they’re right, but this isn’t Dijkstra’s goto. From the Perl manual:
The
goto &NAME
form is quite different from the other forms ofgoto
. In fact, it isn’t a goto in the normal sense at all, and doesn’t have the stigma associated with other gotos. Instead, it exits the current subroutine (losing any changes set bylocal
) and immediately calls in its place the named subroutine using the current value of@_
. […] After thegoto
, not evencaller
will be able to tell that this routine was called first.perlfunc manual page
Computer scientists call this tail call elimination. The bottom line is that this achieves our third goal above: immediately jumping to the new method as if it were originally called.
The other tricky bit is in the line before, when we’re redefining old_method
to point to new_method
while we’re still inside old_method
. (Yes, you can do this.) If you’re running under use warnings
(and we are, and you should), you first need to disable that warning. Later calls to old_method
will go straight to new_method
without logging anything.
And that’s it. The next step after launching this change is to add a story to our backlog to monitor our logs for calls to the old method, and gradually refactor our other repositories. Then we can finally remove the old method wrapper.
Top comments (1)
Thank you for the nice goto callstack trick