Передача метода в качестве параметра в Ruby

118

Я пытаюсь немного повозиться с Руби. Поэтому я пытаюсь реализовать алгоритмы (данные на Python) из книги "Программирование коллективного разума" Ruby.

В главе 8 автор передает метод как параметр. Кажется, это работает в Python, но не в Ruby.

У меня есть метод

def gaussian(dist, sigma=10.0)
  foo
end

и хотите вызвать это другим методом

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

Все что у меня есть ошибка

ArgumentError: wrong number of arguments (0 for 1)
Кристиан Штаде-Шульдт
источник

Ответы:

100

Вам нужен объект proc:

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

Просто обратите внимание, что вы не можете установить аргумент по умолчанию в таком объявлении блока. Поэтому вам нужно использовать знак и установить значение по умолчанию в самом коде процедуры.


Или, в зависимости от вашего объема всего этого, может быть проще передать вместо этого имя метода.

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

В этом случае вы просто вызываете метод, определенный для объекта, а не передаете полный фрагмент кода. В зависимости от того, как вы это структурируете, вам может потребоваться заменить self.sendнаobject_that_has_the_these_math_methods.send


И последнее, но не менее важное: вы можете повесить блок на метод.

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

Но похоже, что вы хотели бы здесь больше многоразовых фрагментов кода.

Алекс Уэйн
источник
1
Я думаю, что второй вариант - лучший вариант (то есть с использованием Object.send ()), недостаток в том, что вам нужно использовать класс для всего этого (что в любом случае вы должны делать в OO :)). Это более СУХОЙ, чем постоянная передача блока (Proc), и вы даже можете передавать аргументы через метод оболочки.
Джимми Стенке,
4
В дополнение, если вы хотите использовать foo.bar(a,b)send, это так foo.send(:bar, a, b). Оператор splat (*) позволяет вам сделать это, foo.send(:bar, *[a,b])если вы обнаружите, что хотите иметь произвольный удлиненный массив аргументов - при условии, что метод bar может их впитать
xxjjnn
100

Комментарии, относящиеся к блокам и процедурам, верны в том смысле, что они более обычны в Ruby. Но вы можете передать метод, если хотите. Вы вызываете, methodчтобы получить метод и .callвызвать его:

def weightedknn( data, vec1, k = 5, weightf = method(:gaussian) )
  ...
  weight = weightf.call( dist )
  ...
end
Дэниел Люкрафт
источник
3
Это интересно. Стоит отметить, method( :<name> )что при преобразовании имени метода в вызываемый символ вы вызываете только один раз. Вы можете сохранить этот результат в переменной или параметре и с этого момента продолжать передавать его дочерним функциям, как и любую другую переменную ...
1
Или, может быть, вместо использования синтаксиса метода в списке аргументов вы можете использовать его при вызове метода следующим образом: weightedknn (data, vec1, k, method (: gaussian))
Яхья
1
Этот метод лучше, чем возиться с процедурой или блоком, поскольку вам не нужно обрабатывать параметры - он просто работает с тем, что хочет метод.
danuker
3
Для завершения, если вы хотите передать метод, определенный где-то еще, сделайте SomewhereElse.method(:method_name). Это круто!
medik
Это может быть отдельный вопрос, но как я могу определить, ссылается ли символ на функцию или что-то еще? Я попытался , :func.classно это простоsymbol
quietContest
46

Вы можете передать метод как параметр с method(:function)way. Вот очень простой пример:

def double (а)
  вернуть * 2 
конец
=> ноль

def method_with_function_as_param (обратный вызов, номер) 
  callback.call (номер) 
конец 
=> ноль

method_with_function_as_param (метод (: двойной), 10) 
=> 20
Патрик Вонг
источник
7
Я столкнулся с проблемой для метода с более сложной областью действия и, наконец, понял, как это сделать, надеюсь, это может кому-то помочь: если ваш метод, например, находится в другом классе, вы должны вызвать последнюю строку кода, например, method_with_function_as_param(Class.method(:method_name),...)а неmethod(:Class.method_name)
V Déhaye 01
Благодаря вашему ответу я обнаружил метод под названием method. Сделал свой день, но, думаю, именно поэтому я предпочитаю функциональные языки, не нужно делать такую ​​акробатику, чтобы получить то, что вы хотите. Все равно рубин копаю
Людовик Куты
25

Обычный способ Ruby сделать это - использовать блок.

Это будет примерно так:

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

И используется как:

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

Этот шаблон широко используется в Ruby.

цыпленок
источник
14

Вы можете использовать &оператор в Methodэкземпляре вашего метода, чтобы преобразовать метод в блок .

Пример:

def foo(arg)
  p arg
end

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

bar(&method(:foo))

Подробнее на http://weblog.raganwald.com/2008/06/what-does-do-when-used-as-unary.html

Джованни Каппеллотто
источник
1

Вы должны вызвать метод "call" объекта функции:

weight = weightf.call( dist )

РЕДАКТИРОВАТЬ: как объяснено в комментариях, этот подход неверен. Это сработает, если вы используете Procs вместо обычных функций.

Tiago
источник
1
Когда он это делает weightf = gaussianв списке аргументов, он фактически пытается выполнить gaussianи назначить результат как значение по умолчанию для весаf. У вызова нет обязательных аргументов и сбоев. Итак, weightf - это еще даже не объект proc с методом вызова.
Алекс Уэйн,
1
Это (то есть неправильный ответ и комментарий, объясняющий почему) на самом деле позволило мне полностью понять принятый ответ, так что спасибо! +1
rmcsharry 04
1

Я бы рекомендовал использовать амперсанд для доступа к именованным блокам внутри функции. Следуя рекомендациям, приведенным в этой статье, вы можете написать что-то вроде этого (это настоящая вырезка из моей рабочей программы):

  # 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 вы можете вызвать эту функцию следующим образом:

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

Если вам не нужно фильтровать свой выбор, вы просто опускаете блок:

@categories = make_select_list( Category ) # selects all categories

Вот и все о мощи блоков Ruby.

vitrums
источник
-5

вы также можете использовать eval и передать метод как строковый аргумент, а затем просто оценить его в другом методе.

Константин
источник
1
Это действительно плохая практика, никогда не делайте этого!
Разработчик
@Developer, почему это считается плохой практикой?
jlesse
Среди причин производительности он evalможет выполнять произвольный код, поэтому он чрезвычайно уязвим для различных атак.
Разработчик