Meneruskan metode sebagai parameter di Ruby

118

Saya mencoba mengotak-atik Ruby. Untuk itu saya mencoba mengimplementasikan algoritma (diberikan dengan Python) dari buku "Programming Collective Intelligence" Ruby.

Dalam bab 8, penulis menggunakan metode a sebagai parameter. Ini sepertinya bekerja dengan Python tetapi tidak di Ruby.

Saya memiliki metode di sini

def gaussian(dist, sigma=10.0)
  foo
end

dan ingin menyebutnya dengan metode lain

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

Yang saya dapatkan hanyalah kesalahan

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

Jawaban:

100

Anda menginginkan objek 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

Perhatikan saja bahwa Anda tidak dapat menyetel argumen default dalam deklarasi blok seperti itu. Jadi Anda perlu menggunakan percikan dan mengatur default di kode proc itu sendiri.


Atau, bergantung pada cakupan semua ini, mungkin lebih mudah untuk meneruskan nama metode sebagai gantinya.

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

Dalam hal ini Anda hanya memanggil metode yang didefinisikan pada sebuah objek daripada mengirimkan potongan kode lengkap. Tergantung pada bagaimana Anda menyusun ini, Anda mungkin perlu mengganti self.senddenganobject_that_has_the_these_math_methods.send


Last but not least, Anda dapat menggantung satu blok dari metode ini.

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

Tapi sepertinya Anda ingin potongan kode yang lebih dapat digunakan kembali di sini.

Alex Wayne
sumber
1
Saya pikir opsi kedua adalah opsi terbaik (yaitu, menggunakan Object.send ()), kelemahannya adalah Anda perlu menggunakan kelas untuk semuanya (yang harus Anda lakukan di OO :)). Ini lebih KERING daripada melewatkan satu blok (Proc) sepanjang waktu, dan Anda bahkan dapat meneruskan argumen melalui metode pembungkus.
Jimmy Stenke
4
Sebagai tambahan, jika Anda ingin melakukan foo.bar(a,b)pengiriman, itu saja foo.send(:bar, a, b). Operator percikan (*) memungkinkan Anda melakukannya foo.send(:bar, *[a,b])jika Anda merasa ingin memiliki array argumen memanjang yang sewenang-wenang - dengan asumsi metode bilah dapat menyerapnya
xxjjnn
100

Komentar yang merujuk pada blok dan Procs benar karena lebih umum di Ruby. Tapi Anda bisa melewatkan metode jika Anda mau. Anda memanggil methoduntuk mendapatkan metode dan .callmenyebutnya:

def weightedknn( data, vec1, k = 5, weightf = method(:gaussian) )
  ...
  weight = weightf.call( dist )
  ...
end
Daniel Lucraft
sumber
3
Ini menarik. Perlu dicatat bahwa Anda method( :<name> )hanya memanggil sekali saat mengubah nama metode Anda menjadi simbol yang dapat dipanggil. Anda dapat menyimpan hasil itu dalam variabel atau parameter dan terus meneruskannya ke fungsi
1
Atau mungkin, alih-alih menggunakan sintaks metode dalam daftar argumen, Anda dapat menggunakannya saat menjalankan metode sebagai berikut: weightedknn (data, vec1, k, method (: gaussian))
Yahya
1
Metode ini lebih baik daripada mengotak-atik proc atau blok, karena Anda tidak perlu menangani parameter - ini hanya berfungsi dengan metode apa pun yang diinginkan.
danuker
3
Sebagai penyelesaian, jika Anda ingin meneruskan metode yang ditentukan di tempat lain, lakukan SomewhereElse.method(:method_name). Itu sangat keren!
medik
Ini mungkin pertanyaannya sendiri tetapi, bagaimana saya bisa menentukan apakah sebuah simbol mereferensikan suatu fungsi atau sesuatu yang lain? Saya mencoba :func.classtetapi itu hanyasymbol
quietContest
46

Anda bisa melewatkan metode sebagai parameter dengan method(:function)cara. Di bawah ini adalah contoh yang sangat sederhana:

def ganda (a)
  mengembalikan * 2 
akhir
=> nihil

def method_with_function_as_param (panggilan balik, angka) 
  callback.call (nomor) 
akhir 
=> nihil

method_with_function_as_param (metode (: ganda), 10) 
=> 20
Patrick Wong
sumber
7
Saya menghadapi masalah untuk metode dengan ruang lingkup yang lebih rumit, dan akhirnya menemukan cara melakukannya, semoga ini dapat membantu seseorang: Jika metode Anda misalnya di kelas lain, Anda harus memanggil baris terakhir kode suka method_with_function_as_param(Class.method(:method_name),...)dan bukanmethod(:Class.method_name)
V . Déhaye
Berkat jawaban Anda, saya menemukan metode yang disebut method. Membuat hari saya menyenangkan tetapi saya rasa itu sebabnya saya lebih suka bahasa fungsional, tidak perlu melakukan akrobat seperti itu untuk mendapatkan apa yang Anda inginkan. Bagaimanapun, saya menggali ruby
Ludovic Kuty
25

Cara biasa Ruby untuk melakukan ini adalah dengan menggunakan blok.

Jadi itu akan menjadi seperti ini:

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

Dan digunakan seperti:

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

Pola ini digunakan secara luas di Ruby.

Membuang
sumber
1

Anda harus memanggil metode "panggilan" dari objek fungsi:

weight = weightf.call( dist )

EDIT: seperti yang dijelaskan di komentar, pendekatan ini salah. Ini akan berfungsi jika Anda menggunakan Procs daripada fungsi normal.

Tiago
sumber
1
Ketika dia melakukannya weightf = gaussiandi daftar arg itu sebenarnya mencoba untuk mengeksekusi gaussiandan menetapkan hasilnya sebagai nilai default dari weightf. Panggilan tidak membutuhkan args dan crash. Jadi weightf bahkan bukan objek proc dengan metode panggilan dulu.
Alex Wayne
1
Ini (mis. Melakukan kesalahan dan komentar yang menjelaskan mengapa) sebenarnya memungkinkan saya untuk sepenuhnya memahami jawaban yang diterima, jadi terima kasih! +1
rmcsharry
1

Saya akan merekomendasikan untuk menggunakan ampersand untuk memiliki akses ke blok bernama dalam suatu fungsi. Mengikuti rekomendasi yang diberikan dalam artikel ini Anda dapat menulis sesuatu seperti ini (ini adalah potongan nyata dari program kerja saya):

  # 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

Selanjutnya, Anda dapat memanggil fungsi ini seperti ini:

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

Jika Anda tidak perlu memfilter pilihan Anda, Anda cukup menghilangkan pencekalan:

@categories = make_select_list( Category ) # selects all categories

Begitu hebatnya kekuatan blok Ruby.

vitrum
sumber
-5

Anda juga bisa menggunakan "eval", dan meneruskan metode sebagai argumen string, lalu cukup evaluasi di metode lain.

Konstantin
sumber
1
Ini benar-benar praktik yang buruk, jangan pernah melakukannya!
Pengembang
@Pengembang mengapa ini dianggap praktik yang buruk?
jlesse
Di antara alasan kinerja, evaldapat mengeksekusi kode arbitrer sehingga sangat rentan terhadap berbagai serangan.
Pengembang