Pemisahan positional dan keyword argument pada Ruby 3.0

Artikel ini menjelaskan rencana incompatibility dari keyword argument pada Ruby 3.0

tl;dr

Pada Ruby 3.0, positional dan keyword argument akan dipisahkan. Ruby 2.7 akan memberi peringatan terhadap perilaku yang akan berubah pada Ruby 3.0. Jika Anda melihat peringatan di bawah ini maka Anda perlu untuk memperbarui kode Anda:

  • Menggunakan argument terakhir sebagai parameter argument sudah usang, atau
  • Melewatkan keyword argument sebagai parameter hash terakhir juga sudah usang, atau
  • Memisahkan argument terakhir ke positional dan keyword argument juga sudah usang

Pada kebanyakan kasus, Anda dapat menghindari compatibility ini dengan menambahkan operator double splat. Operator ini secara langsung melewatkan keyword argument ketimbang sebuah objek Hash. Begitu juga, Anda mungkin menambahkan tanda kurung kurawal {} untuk secara langsung melewatkan sebuah objek Hash ketimbang keyword argument. Baca bagian “Kasus Khusus” di bawah ini untuk lebih detail.

Pada Ruby 3, sebuah method yang mendelegasikan semua argument harus secara langsung mendelegasikan keyword argument dan positional argument. Jika Anda ingin tetap menggunakan perilaku delegasi yang ditemukan pada Ruby 2.7 dan sebelumnya, gunakan ruby2_keywords. Lihat bagian “Menangani delegasi argument” untuk lebih detail.

Kasus Khusus

Berikut adalah kasus yang paling khas. Anda dapat menggunakan operator double splat (**) untuk melewatkan keyword daripada sebuah Hash.

# Method ini hanya menerima sebuah keyword argument
def foo(k: 1)
  p k
end

h = { k: 42 }

# Pemanggilan method ini melewatkan sebuah positional Hash argument
# Pada Ruby 2.7: Hash secara otomatis diubah menjadi sebuah keyword argument
# Pada Ruby 3.0: Pemanggilan ini menyebabkan ArgumentError
foo(h)
  # => demo.rb:11: warning: Using the last argument as keyword parameters is deprecated; maybe ** should be added to the call
  #    demo.rb:2: warning: The called method `foo' is defined here
  #    42

# Jika Anda ingin tetap menggunakan perilaku sebelumnya pada Ruby 3.0, gunakan double splat
foo(**h) #=> 42

Ini adalah kasus lainnya. Anda dapat menggunakan tanda kurung kurawal ({}) untuk melewatkan sebuah objek Hash ketimbang keyword secara langsung.

# Method ini menerima satu positional argument dan sebuah keyword rest argument
def bar(h, **kwargs)
  p h
end

# Pemanggilan ini hanya melewatkan sebuah keyword argument tanpa positional argument
# Pada Ruby 2.7: keyword diubah ke sebuah positional Hash argument
# Pada Ruby 3.0: Pemanggilan ini menyebabkan ArgumentError
bar(k: 42)
  # => demo2.rb:9: warning: Passing the keyword argument as the last hash parameter is deprecated
  #    demo2.rb:2: warning: The called method `bar' is defined here
  #    {:k=>42}

# Jika Anda ingin tetap menggunakan perilaku sebelumnya pada Ruby 3.0, gunakan
# tanda kurung kurawal untuk mengubahnya menjadi sebuah explicit Hash
bar({ k: 42 }) # => {:k=>42}

Apa yang sudah usang?

Pada Ruby 2, keyword argument dapat dianggap sebagai positional Hash argument terakhir dan sebuah positional Hash argument terakhir dapat dianggap sebagai keyword argument.

Karena pengubahan otomatis tersebut, kadang-kadang hal ini sangat kompleks dan sulit seperti yang dideskripsikan pada bagian terakhir. Sehingga, sekarang perilaku tersebut sudah usang pada Ruby 2.7 dan akan dihilangkan pada Ruby 3. Dengan kata lain, keyword argument akan dipisahkan seluruhnya dari positional-nya pada Ruby 3. Ketika Anda ingin melewatkan keyword argument, Anda seharusnya selalu menggunakan foo(k: expr) atau foo(**expr). Jika Anda ingin menerima keyword argument, pada dasarnya Anda harus selalu menggunakan def foo(k: default) atau def foo(k:) atau def foo(**kwargs).

Ingat bahwa Ruby 3.0 tidak membedakan perilaku ketika memanggil sebuah method yang tidak menerima keyword argument dengan method yang menerima keyword argument. Sebagai contoh, kasus berikut ini tidak akan usang dan akan tetap berjalan pada Ruby 3.0. Keyword argument tetap dianggap sebagai sebuah positional Hash argument.

def foo(kwargs = {})
  kwargs
end

foo(k: 1) #=> {:k=>1}

Hal ini karena gaya di atas sangat sering digunakan dan tidak ada ambiguitas saat bagaimana argument seharusnya diperlakukan. Melarang pengubahan ini akan menyebabkan incompatibility tambahan untuk manfaat yang sedikit.

Namun demikian, gaya ini tidak direkomendasikan pada kode baru, kecuali anda sering melewatkan sebuah Hash sebagai sebuah positional argument dan juga menggunakan keyword argument. Jika tidak, gunakan double splat:

def foo(**kwargs)
  kwargs
end

foo(k: 1) #=> {:k=>1}

Akankah kode saya tidak berjalan pada Ruby 2.7?

Jawaban singkatnya adalah “mungkin tidak”.

Perubahan pada Ruby 2.7 didesain sebagai sebuah migrasi menuju 3.0. Pada dasarnya, Ruby 2.7 hanya memperingatkan perilaku yang akan berubah pada Ruby 3, ini termasuk beberapa perubahan incompatible yang kami pertimbangkan sangat kecil. Lihat bagian “Perubahan kecil lainnya” untuk detail.

Kecuali untuk peringatan dan perubahan kecil, Ruby 2.7 mencoba untuk tetap compatible dengan Ruby 2.6. Sehingga, kode Anda mungkin akan berjalan pada Ruby 2.7, walaupun mungkin mengeluarkan peringatan. Dengan menjalankannya pada Ruby 2.7, Anda dapat mengecek jika kode Anda siap untuk Ruby 3.0.

Jika Anda ingin mematikan peringatan deprecation, mohon gunakan command-line argument -W:no-deprecated atau menambahkan Warning[:deprecated] = false pada kode Anda.

Menangani delegasi argument

Ruby 2.6 dan sebelumnya

Pada Ruby 2, Anda dapat menulis sebuah method delegasi dengan menerima sebuah argument *rest dan &block dan melewatkan keduanya ke method tujuan. Keyword argument pada perilaku ini secara tidak langsung ditangani oleh pengubahan otomatis antara positional dan keyword argument.

def foo(*args, &block)
  target(*args, &block)
end

Ruby 3

Anda butuh mendelegasikan keyword argument secara langsung.

def foo(*args, **kwargs, &block)
  target(*args, **kwargs, &block)
end

Kalau tidak, jika anda tidak membutuhkan compatibility dengan Ruby 2.6 atau sebelumnya dan Anda tidak mengubah argument apapun, Anda dapat menggunakan sintaks delegasi baru (...) yang dikenalkan pada Ruby 2.7.

def foo(...)
  target(...)
end

Ruby 2.7

Secara singkat: gunakan Module#ruby2_keywords dan delegasikan *args, &block.

ruby2_keywords def foo(*args, &block)
  target(*args, &block)
end

ruby2_keywords menerima keyword argument sebagai Hash argument terakhir dan melewatkannya sebagai keyword argument ketika memanggil method lain.

Faktanya, Ruby 2.7 memperbolehkan gaya baru delegasi pada kebanyakan kasus. Namun, ada sebuah corner case yang diketahui. Lihat pada bagian berikutnya.

Sebuah compatible delegation yang berjalan pada Ruby 2.6, 2.7, dan 3

Secara singkat: menggunakan Module#ruby2_keywords lagi.

ruby2_keywords def foo(*args, &block)
  target(*args, &block)
end

Sayangnya, kami perlu untuk menggunakan delegasi gaya lama (seperti, tanpa **kwargs) karena Ruby 2.6 dan sebelumnya tidak dapat menangani gaya delegasi baru dengan benar. Ini adalah satu alasan dari pemisahan keyword argument; detail dijelaskan di bagian akhir. ruby2_keywords memperbolehkan Anda untuk menjalankan gaya lama walaupun pada Ruby 2.7 dan 3.0. Karena tidak ada ruby2_keywords pada 2.6 atau sebelumnya, mohon gunakan ruby2_keywords gem atau definisikan sendiri:

def ruby2_keywords(*)
end if RUBY_VERSION < "2.7"

Jika kode Anda tidak harus berjalan pada Ruby 2.6 atau sebelumnya, Anda bisa mencoba gaya baru pada Ruby 2.7. Pada kebanyakan kasusu, ini berjalan. Catat bahwa masih ada corner case berikut:

def target(*args)
  p args
end

def foo(*args, **kwargs, &block)
  target(*args, **kwargs, &block)
end

foo({})       #=> Ruby 2.7: []   ({} dibuang)
foo({}, **{}) #=> Ruby 2.7: [{}] (Anda dapat melewatkan {} dengan secara langsung melewatkan tanpa keyword)

Sebuah Hash argument kosong secara otomatis diubah dan diserap menjadi **kwargs dan pemanggilan delegasi menghilangkan keyword hash kosong, sehinggan tanpa argument dilewatkan ke target. Sejauh yang kami tahu, corner case hanya ini.

Seperti yang tertulis di baris terakhir, Anda dapat menyelesaikan masalah ini dengan menggunakan **{}.

Jika Anda sangat khawatir terkait portability, gunakan ruby2_keywords. (Diakui bahwa Ruby 2.6 dan sebelumnya memiliki banyak corner case pada keyword argument. :-) ruby2_keywords ke depan mungkin dihilangkan setelah masa Ruby 2.6 berakhir. Pada saat tersebut, kami merekomendasikan untuk secara langsung mendelegasikan keyword argument (lihat kode Ruby 3 di atas).

Perubahan kecil lainnya

Ada tiga perubahan kecil terkait keyword argument pada Ruby 2.7.

1. Non-symbol key diperbolehkan pada keyword argument

Pada Ruby 2.6 dan sebelumnya, hanya Symbol key yang diperbolehkan pada keyword argument. Keyword argument dapat menggunakan non-Symbol key pada Ruby 2.7.

def foo(**kwargs)
  kwargs
end
foo("key" => 42)
  #=> Ruby 2.6 dan sebelumnya: ArgumentError: wrong number of arguments
  #=> Ruby 2.7 dan setelahnya: {"key"=>42}

Jika sebuah method menerima baik optional maupun keyword argument, objek Hash yang memiliki Symbol dan non-Symbol key dipisah menjadi dua pada Ruby 2.6. Pada Ruby 2.7, keduanya diterima sebagai keyword karena non-Symbol key diperbolehkan.

def bar(x=1, **kwargs)
  p [x, kwargs]
end

bar("key" => 42, :sym => 43)
  #=> Ruby 2.6: [{"key"=>42}, {:sym=>43}]
  #=> Ruby 2.7: [1, {"key"=>42, :sym=>43}]

# Gunakan tanda kurung kurawal untuk menjaga perilaku
bar({"key" => 42}, :sym => 43)
  #=> Ruby 2.6 and 2.7: [{"key"=>42}, {:sym=>43}]

Ruby 2.7 masih memisahkan hash dengan sebuah peringatan jika sebuah Hash atau keyword argument dengan Symbol dan non-Symbol key ke sebuah method yang menerima keyword secara langsung, tetapi tidak ada keyword rest argument (**kwargs). Perilaku ini akan dihilangkan pada Ruby 3 dan ArgumentError akan muncul.

def bar(x=1, sym: nil)
  p [x, sym]
end

bar("key" => 42, :sym => 43)
# Ruby 2.6 and 2.7: => [{"key"=>42}, 43]
# Ruby 2.7: warning: Splitting the last argument into positional and keyword parameters is deprecated
#           warning: The called method `bar' is defined here
# Ruby 3.0: ArgumentError

2. Double splat dengan sebuah hash kosong (**{}) melewati tanpa argument

Ruby 2.6 dan sebelumnya melewatkan **empty_hash ke sebuah Hash kosong sebagai sebuah positional argument. Ruby 2.7 dan setelahnya akan melewatkan tanpa argument.

def foo(*args)
  args
end

empty_hash = {}
foo(**empty_hash)
  #=> Ruby 2.6 dan sebelumnya: [{}]
  #=> Ruby 2.7 dan setelahnya: []

Catat bahwa foo(**{}) tidak melewatkan apapun baik di Ruby 2.6 maupun Ruby 2.7. Pada Ruby 2.6 dan sebelumnya, **{} dihilangkan oleh parser. Hal ini berlaku sama seperti **empty_hash pada Ruby 2.7 dan setelahnya, memperbolehkan untuk jalan mudah melewatkan tanpa keyword argument ke sebuah method.

Ketika memanggil sebuah method dengan jumlah required positional argument yang kurang pada Ruby 2.7, foo(**empty_hash) melewatkan sebuah hash kosong dengan sebuah peringatan untuk compatible dengan Ruby 2.6. Perilaku ini akan dihilangkan pada Ruby 3.0.

def foo(x)
  x
end

empty_hash = {}
foo(**empty_hash)
  #=> Ruby 2.6 dan sebelumnya: {}
  #=> Ruby 2.7: warning: Passing the keyword argument as the last hash parameter is deprecated
  #             warning: The called method `foo' is defined here
  #=> Ruby 3.0: ArgumentError: wrong number of arguments

3. Sintaks no-keyword-argument (**nil) diperkenalkan

Anda dapat menggunakan **nil pada sebuah definisi method untuk menandakan secara langsung bahwa method menerima tanpa keyword argument. Pemanggilan seperti ini dengan keyword argument akan menghasilkan ArgumentError. (Ini sebenarnya adalah fitur baru, bukan compatibility).

def foo(*args, **nil)
end

foo(k: 1)
  #=> Ruby 2.7 dan setelahnya: no keywords accepted (ArgumentError)

Ini berguna untuk menandakan secara langsung jika method tidak dapat menerima keyword argument. Jika tidak, keyword akan diserap pada rest argument sesuai contoh di atas. Jika Anda memperluas sebuah method untuk menerima keyword argument, method mungkin memiliki incompatibility berikut:

# Jika sebuah method menerima rest argument dan tanpa `**nil`
def foo(*args)
  p args
end

# Keyword yang dilewatkan diubah menjadi sebuah objek Hash (walaupun di Ruby 3.0)
foo(k: 1) #=> [{:k=>1}]

# Jika method diperluas untuk menerima sebuah keyword
def foo(*args, mode: false)
  p args
end

# Pemanggilan yang sudah ada mungkin tidak berjalan
foo(k: 1) #=> ArgumentError: unknown keyword k

Mengapa kami membuat usang konversi otomatis

Konversi otomatis pada mulanya muncul sebagai sebuah ide bagus dan bekerja dengan baik di banyak kasus. Namun, hal ini memiliki banyak corner case dan kami telah menerima banyak laporan bug terhadap perilaku tersebut.

Konversi otomatis tidak bekerja dengan baik ketika sebuah method menerima optional positional argument dan keyword argument. Beberapa orang mengharapkan objek Hash terakhir dianggap sebagai sebuah positional argument dan lainnya mengharapkan diubah menjadi keyword argument.

Berikut adalah salah satu kasus yang sangat membingungkan:

def foo(x, **kwargs)
  p [x, kwargs]
end

def bar(x=1, **kwargs)
  p [x, kwargs]
end

foo({}) #=> [{}, {}]
bar({}) #=> [1, {}]

bar({}, **{}) #=> expected: [{}, {}], actual: [1, {}]

Pada Ruby 2, foo({}) melewatkan sebuah hash kosong sebagai normal argument (contoh, {} ditetapkan sebagai x), sementara bar({}) melewatkan sebuah keyword argument (contoh, {} ditetapkan sebagai kwargs). Sehingga, any_method({}) sangat ambigu.

Masalah yang sama juga berlaku pada method yang menerima rest dan keyword argument. Ini membuat delegasi langsung keyword argument tidak berjalan

def target(*args)
  p args
end

def foo(*args, **kwargs, &block)
  target(*args, **kwargs, &block)
end

foo() #=> Ruby 2.6 dan sebelumnya: [{}]
      #=> Ruby 2.7 dan setelahnya:  []

foo() melewatkan tanpa argument, tetapi target menerima sebuah hash argument kosong pada Ruby 2.6. Hal ini karena method foo mendelegasikan keyword (kwargs) secara langsung. Ketika foo() dipanggil, args adalah sebuah Array kosong, kwargs adalah sebuah Hash kosong, dan block adalah nil. Kemudian, target(*args, **kwargs, &block) melewatkan sebuah Hash kosong sebagai argument karena **kwargs secara otomatis diubah ke sebuah positional Hash argument.

Konversi otomatis tidak hanya membingungkan orang akan tetapi membuat method kurang extensible. Lihat [Feature #14183] untuk lebih detail terkait alasan perubahan perilaku ini dan mengapa pilihan implementasi ini dibuat.

Rujukan

Artikel ini telah diperiksa (atau bahkan co-authored) oleh Jeremy Evans dan Benoit Daloze.

Riwayat

  • Diperbarui 2019-12-25: Pada 2.7.0-rc2, pesan peringatan sedikit diubah dan sebuah API untuk membenamkan peringatan telah ditambahkan.