Currying in Ruby
March 02, 2008Currying is a programming concept I first saw in Haskell. In Haskell, it works something like this:
1 foo :: Int -> Int -> Int 2 foo a b = a + b 3 (foo 1) 2 4 # => 3
What's happening here is when you call (foo 1) it's returning a method that takes 1 more parameter since the original takes 2 and you gave it 1 already and then we send it the second parameter.
Something similar has been done in Ruby but it's not quite as clean as it is in Haskell. I spent some time trying to come up with a cleaner solution and came up with this:
1 class Foo 2 include Curriable 3 def bar(one, two, three) 4 puts "#{one + two + three}" 5 end 6 curriable(:bar) 7 end 8 9 foo = Foo.new 10 11 c = foo.bar(1) 12 13 puts c[2][3] # => 6
Of course that is just the usage of it (the rest is below). It's not exactly perfect, you can't curry anything that expects zero or a variable number of arguments (which doesn't make sense anyway) and you have to use the bracket method to call the method once it's curried. If someone can come up with a cleaner way to actually call it instead of the brackets, that would be nice.
The meat:
1 class CannotCurryException < Exception; end 2 3 module Curriable 4 def self.included(base) 5 base.send(:extend, CurriableMethod) 6 end 7 8 module CurriableMethod 9 def curriable(method_name) 10 # I didn't use here-doc here because it breaks syntax highlighting :( 11 class_eval " 12 alias_method \"_#{method_name}_original\", method_name 13 def #{method_name}(*args) 14 if self.class != CurriedMethod && method(:_#{method_name}_original).arity < 1 15 raise CannotCurryException.new(\"You can't curry a method expecting zero or a variable number of arguments.\") 16 end 17 if args.size < method(:_#{method_name}_original).arity 18 return CurriedMethod.new(self, :#{method_name}, *args).method(:call) 19 else 20 return send(:_#{method_name}_original, *args) 21 end 22 end" 23 end 24 end 25 end 26 27 class CurriedMethod 28 self.instance_methods.each {|m| undef_method(m) unless ["__send__", "class", "send", "__id__", "method"].include?(m) } 29 include Curriable 30 def initialize(object, method, *args) 31 @object = object 32 @method = method 33 @args = args 34 end 35 36 def call(*args) 37 (@object || Kernel).send(@method, *(@args + args)) 38 end 39 curriable(:call) 40 end
Comments
Prototype has added a similar method recently in Prototype. Check out:
http://prototypejs.org/api/function/curry
proc.curry is part of Ruby 1.9
http://groups.google.com/group/ruby-core-google/browse_thread/thread/ece803d2b512830c
Giles,
That's not quite the same thing. All you're doing there is aliasing the + method of Fixnum.
This is equivalent to what you have:
Arya,
They're not even close to equivalent. All YOU're doing there is aliasing the + method of Fixnum. Giles showed you how to pull off a method object. You can do all sorts of funky stuff with it
Or less interesting, but more germane, call it directly:
I'm not sure it's currying per se, but it has to count for something - it got rid of the target object! And for an operation such as integer addition, where there's really no difference between the target and the arguments, I'd say it can qualify.
Pat,
You're right. Giles' example is using a method, which you can unbind and bind like you showed. But it's not currying.
If you take that approach and abstract it to apply to methods of any class and of any fixed argument list length, you may end up with something similar to what I have in the main post.
Anyway, I didn't mean to slash Giles' example if that's what it came off as.
What's happening here is when you call (foo 1) it's returning a method that takes 1 more parameter since the original takes 2 and you gave it 1 already and then we send it the second parameter.
Leave a Comment