Currying in Ruby & How to build one
What is Currying?
Currying(mostly called as pre-loading in python) is a very powerful technique to solve some of calculation related problems. The concept is pretty old in mathematics but still makes huge use cases in the world of functional programming. Fun fact is that it was named after the mathematician Haskel Curry and has nothing to do with cooking. I have used currying in python a lot, especially with decorators as its a powerful value add there. This is a primer into using currying in ruby programming. [skip to custom currying section]
Currying, relies a lot on closures, hence the only way to achieve currying is using either of lambda, proc or a block. For my connivence, I will use lambdas in this article. Let us take problem to solve. We need our own implementation of logger, which does some basic formatting of the message(adding a timestamp) and the forwarding the message to the corresponding logging handler.
Example Curry:
A typical approach in object oriented way would be to have a Logger class which can be initialised with the desired handler object and let it do the formatting and then call the handler object's log method. Let us do the same via lambdas and currying.
First, the logger lambda:
Now we have a lambda logger, that accepts 2 arguments the handler and the message to be logged. Notice the method call curry at the end. All lambda/proc/block are instance of #Proc class and it has a method named curry. Also notice the change in address of the proc instance returned. Let's revisit this later.
As we called our curried lambda with a partial argument, a new lambda is returned with the handler object in the closure. Notice the change in Proc instance address. This was a surprise as lambdas in ruby are argument sensitive and throw an exception when argument count doesn't match.
Now, we can use this console_logger across our application to log messages to the console.
console_logger["Curried log message"] => "2020-01-16: Curried log message"
Similarly, we can have pre-cooked, logger objects across handler to use across the application without having to instantiate a Logger class every time.
This might look initially puzzling but all it does is , whenever we call the lambda with partial arguments , it creates a closure with the partial arguments in scope and returns a new lambda.
Custom Curry method:
Now, let us try to write our own version of curry method to achieve the above, to understand it better.
Step 1:
First, when we created a lambda we got Proc instance of type lambda. This is just an initialisation. Then, when we called curry it returned a new instance. So, all it could have possibly done is create a new lambda and returned the same. This new lambda should be capable of accepting any partial arguments supplied later and create a closure around it.So, lets override Proc class with method my_curry to do the same
class Proc
def my_curry
# when my_curry is called: create a new proc and return
curried = proc do |*partial_args|
end
return curried
end
end
Step 2:
When partial arguments were passed, we saw a new instance of Proc being returned. So, the partial arguments must have been stored in closure and new proc would be returned. Lets do the same
class Proc
def my_curry
# when my_curry is called: create a new proc and return
curried = proc do |*partial_args|
# when curried proc is called: create a closure with partial
arguments and then return a new proc/lambda
new_lambda = lambda do |*remaining_args|
end
return new_lambda
end
return curried
end
end
Step 3:
When we called with the remaining arguments, the original lambda has to get executed with all arguments together. So, we can combine the currently passed arguments and the previously closured arguments to create a full argument list.
Here's the full code and the link to the gist:
Disclaimer: As currying is a default of ruby, the actual implementation will be in C/java depending on the type of Ruby we use. In my case, its a C-Ruby and the code for the same(taken from pry-doc):
Unless I'm missing something obvious, `console_logger = logger[ConsoleHandler.new]` should be `console_logger = curried_logger[ConsoleHandler.new]`, right?