|go ahead... be a heretic|
Near-free function currying in Perlby tmoertel (Chaplain)
|on Nov 17, 2004 at 02:50 UTC||Need Help??|
Currying (which has been discussed before on Perl Monks) makes it easy to specialize functions by pre-binding some of their arguments to given values. It might not sound impressive, but after you have coded with currying for a while, it's hard to live without it.
Unfortunately, currying in Perl is a bit awkward, typically relying on a helper function of sorts to make the currying happen:
In this meditation, we'll do away with helper functions and reduce the cost of currying to almost zero. Together, we'll write AutoCurry, the tiny module that makes it happen.
(Update: If you're wondering why I didn't use some of the existing currying modules or why I don't use prototypes or annotations to make my implementation more like "real" currying, please read my comment on this subject, which explains why I think the style of currying I present below makes more sense for Perl (5) than does signature-based currying.)
(Update: tweaked code to reduce reliance upon INIT-time processing (thanks diotalevi!))
Our goal is to make currying free, like it is in some other programming languages. (In Haskell, for example, you don't need to say that you want to curry a function; you just call it with fewer than the expected number of arguments, and the currying happens automatically.)
Why do this in Perl?
Before getting to AutoCurry, let's spend a few moments justifying the exercise. Why should we try to reduce the cost of currying in Perl? The answer is because currying reduces the cost of reusing functions, making reuse practical in more situations, and that in turn reduces the cost of programming in general. In short, with currying we reinvent the wheel less. If we can reduce the cost of currying further, we might be able to achieve further cost reductions.
As a motivating example, consider the case of logging. Let's say that we have the following generalized logging function:
It's a simple function for the sake of our example, but let's imagine that it's complex and would be costly to rewrite.
Let's further say that later we're working with a server framework that lets us configure it with a logging function to use when the server emits diagnostic messages:
The application server expects $mylogger to be a logging function that takes a single argument, the message to be logged.
It would be nice to be able to reuse our existing, 3-argument logging function for this purpose. We can do this by adapting it to the application server's expectations. Because the application server expects a 1-argument function and we have a 3-argument function, we must specialize away the extra arguments. We'll do this by binding $fh to STDERR and $heading to "app server".
In some programming languages, we would need to write a wrapper function to specialize the function:
But Perl gives us a less-expensive way. We can use an anonymous subroutine to create an on-the-fly wrapper, tailored to our needs:
Still, we can do better. We can use a currying helper function to take the cost down another notch and also to make clear our intent to specialize an existing function:
That's pretty good, but specialization in Perl is still more expensive than in some other languages. It would be great to reduce the cost to the bare minimum, where a regular function call is automatically curried if it doesn't receive all of the arguments it wants:
That's the ultimate goal: zero-cost currying.
The idea behind AutoCurryAutoCurry gets us very close to the goal. We can't quite make it all the way because functions in Perl can accept varying numbers of arguments, and thus it's hard for us to determine reliably when currying is implied by analyzing function calls. For this reason, we take the practical road and rely upon a hint from the programmer to tell us when currying is expected. (Seen from this light, calling the curry helper function could be considered a rather expensive hint. We want to make the hint less expensive.)
The hint that we will use is to append the suffix "_c" to any function call that we want to have currying semantics. To show how it works with our running example:
That's only two characters away from the ideal, which is probably as close as we can practically make it.
Underneath, the implementation relies upon double-currying and some symbol-table manipulation to create curried variants of our normal functions.
Let's walk through the strategy. First, we need a run-of-the-mill currying helper:
Then, to create a currying variant of a normal function, we "double curry" it and store the resulting function in the symbol table under the appropriate _c name:
In essence, each _c function is a partially applied call to curry that specializes the corresponding normal, non-curried function by calling curry again.
Now, to make the approach cost effective, all we need to do is automate it and bring it to a larger scale.
Mass productionFor maximum convenience, we would like to curry-enable every function in our namespace automatically. The first step, then, is to scan our namespace for functions. We can do this by scanning its symbol table and extracting the names associated with non-empty CODE slots:
(Note that we skip functions whose names start with an underscore or are ALL CAPS. Such functions are often system routines that we don't have reason to curry.)
To see how the function works, let's try it on a small package:
Now, all that's left is to iterate over the names and create corresponding _c versions that implement our double-curried strategy:
And that's the essence of AutoCurry.
To wrap it up, we'll place everything in the AutoCurry package, along with some documentation and a few extra helper functions. As a further convenience, the module will accept instructions about what to auto-curry via its import list:
Implementing the import function and robustifying the code above is straightforward, and so I'll stop the meditation here. (If you're curious, I have included the complete code for the module below. It contains fewer than sixty lines of code.)
Thanks for taking the time to read this meditation. If you have any criticisms or comments, please let me know. Also, if you can help me improve my writing, I would greatly appreciate your suggestions.
The code for AutoCurry.pm