unbound imagination - a blog about programming

Currying in Ruby

March 02, 2008

Currying 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

posted by Eric Anderson on 03/03/08 05:50 AM PST

Prototype has added a similar method recently in Prototype. Check out:

http://prototypejs.org/api/function/curry

posted by James on 03/03/08 07:35 AM PST

proc.curry is part of Ruby 1.9

http://groups.google.com/group/ruby-core-google/browse_thread/thread/ece803d2b512830c

posted by giles bowkett on 03/03/08 03:05 PM PST
   1  add2 = 2.method(:+)
   2  add2[3] # 5
   3  
   4  class Fixnum
   5    def adder
   6      self.method(:+)
   7    end 
   8  end
   9  
  10  2.adder[3] # 5
posted by Arya Asemanfar on 03/03/08 03:11 PM PST

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:

   1  class Fixnum
   2    alias_method :adder, :+
   3  end
   4  2.adder(3)
posted by Pat Maddox on 03/03/08 03:45 PM PST

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

   1  adder = 2.method(:+)
   2  adder.unbind.bind(7).call 3
   3  # => 10

Or less interesting, but more germane, call it directly:

   1  adder[4] # => 6

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.

posted by Arya Asemanfar on 03/03/08 04:04 PM PST

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.

posted by giles bowkett on 03/03/08 09:03 PM PST
   1  foo :: Int -> Int -> Int
   2  foo a b = a + b
   3  (foo 1) 2 # => 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.

   1  it "returns a proc that takes 1 more parameter" do
   2    (foo[1].class).should == Proc
   3    foo[1][2].should == 3
   4  end
   5  
   6  def foo 
   7    lambda do |arg|
   8      lambda do |second_arg|
   9        arg + second_arg
  10      end 
  11    end 
  12  end
  13  
  14  # http://blog.jcoglan.com/2008/01/10/deriving-the-y-combinator/
  15  
  16  def y(&f)
  17    lambda { |g| g[g] } [ 
  18      lambda do |h| 
  19        lambda { |*args| f[h[h]][*args] }
  20      end 
  21    ]
  22  end
  23  
  24  def bar 
  25    y do |method|
  26      lambda do |arguments|
  27        if 0 == arguments.size
  28          0
  29        else
  30          arguments.slice!(0) + method[arguments]
  31        end 
  32      end 
  33    end 
  34  end
  35  
  36  bar[[2,3]] # => 5
  37  bar[[2,3,5,8,13]] # => 31
  38  bar[[]] # => 0

Leave a Comment