Monday, March 28, 2011

Passing a method as a parameter in Ruby

I am trying to mess around a little bit with Ruby. Therefor I try to implement the algorithms (given in Python) from the book "Programming Collective Intelligence" Ruby.

In chapter 8 the author passes a method a as parameter. This seems to work in Python but not in Ruby.

I have here the method

def gaussian(dist, sigma=10.0)
  foo
end

and want to call this with another method

def weightedknn(data, vec1, k = 5, weightf = gaussian)
  foo
  weight = weightf(dist)
  foo
end

All I got is an error

ArgumentError: wrong number of arguments (0 for 1)
From stackoverflow
  • You have to call the method "call" of the function object:

    weight = weightf.call(dist)
    

    EDIT: as explained in the comments, this approach is wrong. It would work if you're using Procs instead of normal functions.

    Squeegy : When he does `weightf = gaussian` in the arg list it's actually trying to execute `gaussian` and assign the result as the default value of weightf. The call doesn't have required args and crashes. So weightf is not even a proc object with a call method just yet.
    Tiago : Oh, I got it. Thanks for correcting me =)
  • You want a proc object:

    gaussian = Proc.new do |dist, *args|
      sigma = args.first || 10.0
      ...
    end
    
    def weightedknn(data, vec1, k = 5, weightf = gaussian)
      ...
      weight = weightf.call(dist)
      ...
    end
    

    Just note that you can't set a default argument in a block declaration like that. So you need to use a splat and setup the default in the proc code itself.


    Or, depending on your scope of all this, it may be easier to pass in a method name instead.

    def weightedknn(data, vec1, k = 5, weightf = :gaussian)
      ...
      weight = self.send(weightf)
      ...
    end
    

    In this case you are just calling a method that is defined on an object rather than passing in a complete chunk of code. Depending on how you structure this you may need replace self.send with object_that_has_the_these_math_methods.send


    Last but not least, you can hang a block off the method.

    def weightedknn(data, vec1, k = 5)
      ...
      weight = 
        if block_given?
          yield(dist)
        else
          gaussian.call(dist)
        end
      end
      ...
    end
    
    wegihtedknn(foo, bar) do |dist|
      # square the dist
      dist * dist
    end
    

    But it sounds like you would like more reusable chunks of code here.

    Jimmy Stenke : I think that second option is the best option (that is, using Object.send()), the drawback is that you need to use a class for all of it (which is how you should do in OO anyway :)). It is more DRY than passing a block (Proc) all the time, and you could even pass arguments trough the wrapper method.
  • The normal Ruby way to do this is to use a block.

    So it would be something like:

    def weightedknn(data, vec1, k = 5)
      foo
      weight = yield(dist)
      foo
    end
    

    And used like:

    weightenknn(data, vec1) {|dist| gaussian(dist)}
    

    This pattern is used extensively in Ruby.

  • The comments referring to blocks and Procs are correct in that they are more usual in Ruby. But you can pass a method if you want. You call method to get the method and .call to call it:

    def weightedknn(data, vec1, k = 5, weightf = method(:gaussian))
      ...
      weight = weightf.call(dist)
      ...
    end
    

0 comments:

Post a Comment