Passando um método como parâmetro em Ruby

117

Estou tentando mexer um pouco com Ruby. Para tanto procuro implementar os algoritmos (dados em Python) do livro "Programming Collective Intelligence" Ruby.

No capítulo 8 o autor passa um método a como parâmetro. Isso parece funcionar em Python, mas não em Ruby.

Eu tenho aqui o método

def gaussian(dist, sigma=10.0)
  foo
end

e deseja chamar isso com outro método

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

Tudo que eu tenho é um erro

ArgumentError: wrong number of arguments (0 for 1)
Christian Stade-Schuldt
fonte

Respostas:

100

Você quer um objeto 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

Apenas observe que você não pode definir um argumento padrão em uma declaração de bloco como essa. Portanto, você precisa usar um splat e configurar o padrão no próprio código proc.


Ou, dependendo do escopo de tudo isso, pode ser mais fácil passar um nome de método.

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

Nesse caso, você está apenas chamando um método definido em um objeto, em vez de passar um pedaço completo de código. Dependendo de como você estrutura isso, você pode precisar substituir self.sendporobject_that_has_the_these_math_methods.send


Por último, mas não menos importante, você pode suspender um bloco do método.

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

Mas parece que você gostaria de pedaços de código mais reutilizáveis ​​aqui.

Alex Wayne
fonte
1
Acho que a segunda opção é a melhor opção (ou seja, usando Object.send ()), a desvantagem é que você precisa usar uma classe para tudo isso (que é como você deve fazer em OO de qualquer maneira :)). É mais DRY do que passar um bloco (Proc) o tempo todo, e você pode até passar argumentos pelo método wrapper.
Jimmy Stenke
4
Além disso, se você quiser fazer foo.bar(a,b)com o envio, é foo.send(:bar, a, b). O operador splat (*) permite que você faça foo.send(:bar, *[a,b])se quiser ter uma matriz de argumentos de comprimento arbitrário - assumindo que o método bar pode absorvê-los
xxjjnn
99

Os comentários referentes a blocos e Procs estão corretos por serem mais usuais em Ruby. Mas você pode passar um método, se quiser. Você chama methodpara obter o método e .callchamá-lo:

def weightedknn( data, vec1, k = 5, weightf = method(:gaussian) )
  ...
  weight = weightf.call( dist )
  ...
end
Daniel Lucraft
fonte
3
Isto é interessante. É importante notar que você chama method( :<name> )apenas uma vez ao converter o nome do seu método em um símbolo que pode ser chamado . Você pode armazenar esse resultado em uma variável ou parâmetro e continuar passando para funções
1
Ou talvez, em vez de usar a sintaxe do método na lista de argumentos, você pode usá-la enquanto invoca o método da seguinte maneira: weightedknn (data, vec1, k, method (: gaussian))
Yahya
1
Este método é melhor do que mexer em um proc ou bloco, já que você não precisa manipular parâmetros - ele apenas funciona com o que o método quiser.
danuker
3
Para conclusão, se você quiser passar um método definido em outro lugar, faça SomewhereElse.method(:method_name). Isso é bem legal!
medik
Esta pode ser sua própria questão, mas como posso determinar se um símbolo faz referência a uma função ou outra coisa? Eu tentei, :func.classmas é apenas umsymbol
quietContest
46

Você pode passar um método como parâmetro com method(:function)way. Abaixo está um exemplo muito simples:

def double (a)
  retornar a * 2 
fim
=> nulo

def method_with_function_as_param (callback, número) 
  callback.call (número) 
fim 
=> nulo

method_with_function_as_param (method (: double), 10) 
=> 20
Patrick Wong
fonte
7
Eu enfrentei um problema para um método com um escopo mais complicado e finalmente descobri como fazer, espero que isso possa ajudar alguém: Se o seu método está, por exemplo, em outra classe, você deve chamar a última linha de código como method_with_function_as_param(Class.method(:method_name),...)e nãomethod(:Class.method_name)
V . Déhaye 01 de
Graças à sua resposta, descobri o método chamado method. Fez meu dia, mas acho que é por isso que prefiro linguagens funcionais, não há necessidade de fazer tais acrobacias para conseguir o que deseja. Enfim, eu escavo o rubi
Ludovic Kuty
25

A maneira normal do Ruby de fazer isso é usar um bloco.

Então, seria algo como:

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

E usado como:

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

Este padrão é usado extensivamente em Ruby.

Mandril
fonte
1

Você deve chamar o método "call" do objeto de função:

weight = weightf.call( dist )

EDITAR: conforme explicado nos comentários, essa abordagem está errada. Funcionaria se você estiver usando Procs em vez de funções normais.

Tiago
fonte
1
Quando ele faz isso weightf = gaussianna lista de argumentos, ele está na verdade tentando executar gaussiane atribuir o resultado como o valor padrão de weightf. A chamada não requer argumentos e travamentos. Portanto, weightf ainda não é um objeto proc com um método de chamada.
Alex Wayne
1
Isso (ou seja, fazer errado e o comentário explicando o porquê) realmente me permitiu entender completamente a resposta aceita, então, obrigado! +1
rmcsharry
1

Eu recomendaria usar o e comercial para ter acesso a blocos nomeados dentro de uma função. Seguindo as recomendações fornecidas neste artigo, você pode escrever algo assim (este é um recado real do meu programa de trabalho):

  # 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

Depois de responder, você pode chamar essa função assim:

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

Se você não precisa filtrar sua seleção, simplesmente omite o bloco:

@categories = make_select_list( Category ) # selects all categories

Tanto para o poder dos blocos Ruby.

vitrums
fonte
-5

você também pode usar "eval" e passar o método como um argumento de string e, em seguida, simplesmente evalá-lo no outro método.

Konstantin
fonte
1
Esta é uma prática muito ruim, nunca faça isso!
Desenvolvedor
@Developer por que isso é considerado uma prática ruim?
jlesse
Entre os motivos de desempenho, o evalpode executar código arbitrário, por isso é extremamente vulnerável a vários ataques.
Desenvolvedor