141

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)
Christian Stade-Schuldt
  • 4,671
  • 7
  • 35
  • 30

9 Answers9

114

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
Boris Stitnicky
  • 12,444
  • 5
  • 57
  • 74
Daniel Lucraft
  • 7,196
  • 6
  • 34
  • 35
  • 3
    This is interesting. It's worth noting that you call `method( : )` just once when converting your method name to a callable symbol. You can store that result in a variable or a parameter and keep passing it to child functions like any other variable from then on... –  May 03 '14 at 10:42
  • 1
    Or maybe, instead of using the _method_ syntax in the argument list, you can use it while invoking the method as follows : weightedknn( data, vec1, k, method( :gaussian) ) – Yahya Aug 25 '15 at 11:07
  • 1
    This method is better than mucking around with a proc or block, since you don't have to handle parameters - it just works with whatever the method wants. – danuker Jun 02 '16 at 11:52
  • 9
    For completion, if you want to pass a method defined somewhere else, do `SomewhereElse.method(:method_name)`. That's pretty cool! – medik Nov 16 '16 at 23:14
  • 1
    This may be its own question but, how can I determine if a symbol references a function or something else? I tried `:func.class` but that's just a `symbol` – crackpotHouseplant Jun 30 '17 at 22:36
  • awesome @medik. Love it. Or you just pass the object – flp Jan 17 '23 at 15:46
106

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

weightedknn(foo, bar) do |dist|
  # square the dist
  dist * dist
end

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

Nathaniel Ford
  • 20,545
  • 20
  • 91
  • 102
Alex Wayne
  • 178,991
  • 47
  • 309
  • 337
  • 1
    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. – Jimmy Stenke Feb 07 '09 at 12:51
  • 5
    As an addition, if you want to do `foo.bar(a,b)` with send, it is `foo.send(:bar, a, b)`. The splat (*) operator allows you to do `foo.send(:bar, *[a,b])` should you find you want to have an arbitrary lengthed array of arguments - assuming the bar method can soak them up – xxjjnn Mar 13 '14 at 14:47
58

You can pass a method as parameter with method(:function) way. Below is a very simple example:

def double(a)
  return a * 2 
end
=> nil

def method_with_function_as_param( callback, number) 
  callback.call(number) 
end 
=> nil

method_with_function_as_param( method(:double) , 10 ) 
=> 20
rmcsharry
  • 5,363
  • 6
  • 65
  • 108
Patrick Wong
  • 589
  • 4
  • 2
  • 9
    I faced an issue for a method with a more complicated scope, and finally figured out how to do, hope this can help someone : If your method is for example in another class, you should call the last line of code like `method_with_function_as_param(Class.method(:method_name),...)` and not `method(:Class.method_name)` – V. Déhaye Feb 01 '17 at 15:31
  • Thanks to your answer, I discovered the method called `method`. Made my day but I guess that's why I prefer functional languages, no need to make such acrobatics to get what you want. Anyway, I dig ruby – Ludovic Kuty Mar 19 '20 at 16:43
26

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:

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

This pattern is used extensively in Ruby.

Daniel Harvey
  • 381
  • 5
  • 16
Chuck
  • 234,037
  • 30
  • 302
  • 389
17

You can use the & operator on the Method instance of your method to convert the method to a block.

Example:

def foo(arg)
  p arg
end

def bar(&block)
  p 'bar'
  block.call('foo')
end

bar(&method(:foo))

More details at http://weblog.raganwald.com/2008/06/what-does-do-when-used-as-unary.html

Giovanni Cappellotto
  • 4,597
  • 1
  • 30
  • 33
2

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.

Boris Stitnicky
  • 12,444
  • 5
  • 57
  • 74
Tiago
  • 9,457
  • 5
  • 39
  • 35
  • 1
    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. – Alex Wayne Feb 07 '09 at 00:27
  • 1
    This (ie. doing it wrong and the comment explaining why) actually allowed me to fully understand the accepted answer, so thanks! +1 – rmcsharry Jul 04 '19 at 09:16
1

I would recommend to use ampersand to have an access to named blocks within a function. Following the recommendations given in this article you can write something like this (this is a real scrap from my working program):

  # Returns a valid hash for html form select element, combined of all entities
  # for the given +model+, where only id and name attributes are taken as
  # values and keys correspondingly. Provide block returning boolean if you
  # need to select only specific entities.
  #
  # * *Args*    :
  #   - +model+ -> ORM interface for specific entities'
  #   - +&cond+ -> block {|x| boolean}, filtering entities upon iterations
  # * *Returns* :
  #   - hash of {entity.id => entity.name}
  #
  def make_select_list( model, &cond )
    cond ||= proc { true } # cond defaults to proc { true }
    # Entities filtered by cond, followed by filtration by (id, name)
    model.all.map do |x|
      cond.( x ) ? { x.id => x.name } : {}
    end.reduce Hash.new do |memo, e| memo.merge( e ) end
  end

Afterwerds, you can call this function like this:

@contests = make_select_list Contest do |contest|
  logged_admin? or contest.organizer == @current_user
end

If you don't need to filter your selection, you simply omit the block:

@categories = make_select_list( Category ) # selects all categories

So much for the power of Ruby blocks.

Boris Stitnicky
  • 12,444
  • 5
  • 57
  • 74
vitrums
  • 502
  • 4
  • 12
0

Similarly to a Proc or a method call, you can also pass a lambda as weightf parameter :

def main
  gaussian = -> (params) {
    ...
  }
  weightedknn(data, vec1, k = 5, gaussian, params)
  # Use symbol :gaussian if method exists instead
end

def weightedknn(data, vec1, k = 5, weightf, params)
  ...
  weight = weightf.call(params)
  ...
end
h0ly
  • 161
  • 1
  • 8
-8

you also can use "eval", and pass the method as a string argument, and then simply eval it in the other method.

Konstantin
  • 2,983
  • 3
  • 33
  • 55