Serangkaian Kemungkinan: Panduan untuk Pencocokan Pola Ruby

Diterbitkan: 2022-03-11

Pencocokan pola adalah fitur besar baru yang datang ke Ruby 2.7. Itu telah dikomit ke trunk sehingga siapa pun yang tertarik dapat menginstal Ruby 2.7.0-dev dan memeriksanya. Harap diingat bahwa tidak satu pun dari ini yang diselesaikan dan tim pengembang sedang mencari umpan balik sehingga jika Anda memilikinya, Anda dapat memberi tahu pembuat komitmen sebelum fitur tersebut benar-benar keluar.

Saya harap Anda akan mengerti apa itu pencocokan pola dan bagaimana menggunakannya di Ruby setelah membaca artikel ini.

Apa itu Pencocokan Pola?

Pencocokan pola adalah fitur yang umum ditemukan dalam bahasa pemrograman fungsional. Menurut dokumentasi Scala, pencocokan pola adalah “mekanisme untuk memeriksa nilai terhadap suatu pola. Pertandingan yang sukses juga dapat mendekonstruksi nilai menjadi bagian-bagian penyusunnya.”

Ini tidak menjadi bingung dengan Regex, pencocokan string, atau pengenalan pola. Pencocokan pola tidak ada hubungannya dengan string, melainkan struktur data. Pertama kali saya menemukan pencocokan pola sekitar dua tahun lalu ketika saya mencoba Elixir. Saya sedang mempelajari Elixir dan mencoba memecahkan algoritma dengannya. Saya membandingkan solusi saya dengan yang lain dan menyadari bahwa mereka menggunakan pencocokan pola, yang membuat kode mereka jauh lebih ringkas dan lebih mudah dibaca.

Karena itu, pencocokan pola sangat berkesan bagi saya. Seperti inilah pencocokan pola di Elixir:

 [a, b, c] = [:hello, "world", 42] a #=> :hello b #=> "world" c #=> 42

Contoh di atas terlihat sangat mirip dengan beberapa tugas di Ruby. Namun, ini lebih dari itu. Itu juga memeriksa apakah nilainya cocok atau tidak:

 [a, b, 42] = [:hello, "world", 42] a #=> :hello b #=> "world"

Dalam contoh di atas, angka 42 di sisi kiri bukanlah variabel yang ditugaskan. Ini adalah nilai untuk memeriksa bahwa elemen yang sama dalam indeks tertentu cocok dengan yang ada di sisi kanan.

 [a, b, 88] = [:hello, "world", 42] ** (MatchError) no match of right hand side value

Dalam contoh ini, alih-alih nilai yang ditetapkan, MatchError dimunculkan sebagai gantinya. Ini karena angka 88 tidak cocok dengan angka 42.

Ini juga berfungsi dengan peta (yang mirip dengan hash di Ruby):

 %{"name": "Zote", "title": title } = %{"name": "Zote", "title": "The mighty"} title #=> The mighty

Contoh di atas memeriksa bahwa nilai name kunci adalah Zote , dan mengikat nilai title variabel.

Konsep ini bekerja sangat baik ketika struktur datanya kompleks. Anda dapat menetapkan variabel Anda dan memeriksa nilai atau tipe semua dalam satu baris.

Selain itu, Ini juga memungkinkan bahasa yang diketik secara dinamis seperti Elixir untuk memiliki metode yang berlebihan:

 def process(%{"animal" => animal}) do IO.puts("The animal is: #{animal}") end def process(%{"plant" => plant}) do IO.puts("The plant is: #{plant}") end def process(%{"person" => person}) do IO.puts("The person is: #{person}") end

Bergantung pada kunci hash argumen, metode yang berbeda dapat dieksekusi.

Mudah-mudahan, itu menunjukkan betapa kuatnya pencocokan pola. Ada banyak upaya untuk membawa pencocokan pola ke dalam Ruby dengan permata seperti noaidi, qo, dan egison-ruby.

Ruby 2.7 juga memiliki implementasinya sendiri yang tidak jauh berbeda dengan permata ini, dan inilah yang sedang dilakukan saat ini.

Sintaks Pencocokan Pola Ruby

Pencocokan pola di Ruby dilakukan melalui pernyataan case . Namun, alih-alih menggunakan when yang biasa, kata kunci in digunakan sebagai gantinya. Ini juga mendukung penggunaan pernyataan if atau unless :

 case [variable or expression] in [pattern] ... in [pattern] if [expression] ... else ... end

Pernyataan kasus dapat menerima variabel atau ekspresi dan ini akan dicocokkan dengan pola yang disediakan dalam klausa in . Jika atau kecuali pernyataan juga dapat diberikan setelah pola. Pemeriksaan kesetaraan di sini juga menggunakan === seperti pernyataan kasus normal. Ini berarti Anda dapat mencocokkan subset dan instance dari kelas. Berikut adalah contoh bagaimana Anda menggunakannya:

Array yang Cocok

 translation = ['th', 'เต้', 'ja', 'テイ'] case translation in ['th', orig_text, 'en', trans_text] puts "English translation: #{orig_text} => #{trans_text}" in ['th', orig_text, 'ja', trans_text] # this will get executed puts "Japanese translation: #{orig_text} => #{trans_text}" end

Dalam contoh di atas, translation variabel dicocokkan dengan dua pola:

['th', orig_text, 'en', trans_text] dan ['th', orig_text, 'ja', trans_text] . Apa yang dilakukan adalah memeriksa apakah nilai dalam pola cocok dengan nilai dalam variabel translation di setiap indeks. Jika nilainya cocok, ia menetapkan nilai dalam variabel translation ke variabel dalam pola di setiap indeks.

Animasi Pencocokan Pola Ruby: Array Pencocokan

Pencocokan Hash

 translation = {orig_lang: 'th', trans_lang: 'en', orig_txt: 'เต้', trans_txt: 'tae' } case translation in {orig_lang: 'th', trans_lang: 'en', orig_txt: orig_txt, trans_txt: trans_txt} puts "#{orig_txt} => #{trans_txt}" end

Pada contoh di atas, variabel translation sekarang menjadi hash. Itu dicocokkan dengan hash lain dalam klausa in . Apa yang terjadi adalah bahwa pernyataan kasus memeriksa apakah semua kunci dalam pola cocok dengan kunci dalam variabel translation . Itu juga memeriksa bahwa semua nilai untuk setiap kunci cocok. Kemudian memberikan nilai ke variabel dalam hash.

Animasi Pencocokan Pola Ruby: Array Pencocokan

Subset yang cocok

Pemeriksaan kualitas yang digunakan dalam pencocokan pola mengikuti logika === .

Beberapa Pola

  • | dapat digunakan untuk mendefinisikan beberapa pola untuk satu blok.
 translation = ['th', 'เต้', 'ja', 'テイ'] case array in {orig_lang: 'th', trans_lang: 'ja', orig_txt: orig_txt, trans_txt: trans_txt} | ['th', orig_text, 'ja', trans_text] puts orig_text #=> เต้ puts trans_text #=> テイend

Dalam contoh di atas, variabel translation cocok dengan {orig_lang: 'th', trans_lang: 'ja', orig_txt: orig_txt, trans_txt: trans_txt} dan hash ['th', orig_text, 'ja', trans_text] Himpunan.

Ini berguna ketika Anda memiliki tipe struktur data yang sedikit berbeda yang mewakili hal yang sama dan Anda ingin kedua struktur data tersebut mengeksekusi blok kode yang sama.

Tugas Panah

Dalam hal ini, => dapat digunakan untuk menetapkan nilai yang cocok ke variabel.

 case ['I am a string', 10] in [Integer, Integer] => a # not reached in [String, Integer] => b puts b #=> ['I am a string', 10] end

Ini berguna ketika Anda ingin memeriksa nilai di dalam struktur data tetapi juga mengikat nilai-nilai ini ke variabel.

Pin Operator

Di sini, operator pin mencegah variabel agar tidak dipindahkan.

 case [1,2,2] in [a,a,a] puts a #=> 2 end

Dalam contoh di atas, variabel a dalam pola dicocokkan dengan 1, 2, dan kemudian 2. Ini akan ditugaskan ke 1, lalu 2, lalu ke 2. Ini bukan situasi yang ideal jika Anda ingin memeriksa bahwa semua nilai dalam array adalah sama.

 case [1,2,2] in [a,^a,^a] # not reached in [a,b,^b] puts a #=> 1 puts b #=> 2 end

Ketika operator pin digunakan, ia mengevaluasi variabel alih-alih menugaskannya kembali. Pada contoh di atas, [1,2,2] tidak cocok dengan [a,^a,^a] karena pada indeks pertama, a ditetapkan ke 1. Pada indeks kedua dan ketiga, a dievaluasi menjadi 1, tetapi dicocokkan dengan 2.

Namun [a,b,^b] cocok dengan [1,2,2] karena a ditetapkan ke 1 di indeks pertama, b ditugaskan ke 2 di indeks kedua, lalu ^b, yang sekarang 2, dicocokkan dengan 2 di indeks ketiga sehingga lolos.

 a = 1 case [2,2] in [^a,^a] #=> not reached in [b,^b] puts b #=> 2 end

Variabel dari luar pernyataan kasus juga dapat digunakan seperti yang ditunjukkan pada contoh di atas.

Garis bawah ( _ ) Operator

Garis bawah ( _ ) digunakan untuk mengabaikan nilai. Mari kita lihat dalam beberapa contoh:

 case ['this will be ignored',2] in [_,a] puts a #=> 2 end
 case ['a',2] in [_,a] => b puts a #=> 2 Puts b #=> ['a',2] end

Dalam dua contoh di atas, nilai apa pun yang cocok dengan _ lolos. Dalam pernyataan kasus kedua, => operator menangkap nilai yang telah diabaikan juga.

Gunakan Kasus untuk Pencocokan Pola di Ruby

Bayangkan Anda memiliki data JSON berikut:

 { nickName: 'Tae' realName: {firstName: 'Noppakun', lastName: 'Wongsrinoppakun'} username: 'tae8838' }

Di proyek Ruby Anda, Anda ingin mengurai data ini dan menampilkan nama dengan ketentuan berikut:

  1. Jika nama pengguna ada, kembalikan nama pengguna.
  2. Jika nama panggilan, nama depan, dan nama belakang ada, kembalikan nama panggilan, nama depan, dan kemudian nama belakang.
  3. Jika nama panggilan tidak ada, tetapi nama depan dan belakang ada, kembalikan nama depan dan kemudian nama belakang.
  4. Jika tidak ada ketentuan yang berlaku, kembalikan "Pengguna Baru."

Ini adalah bagaimana saya akan menulis program ini di Ruby sekarang:

 def display_name(name_hash) if name_hash[:username] name_hash[:username] elsif name_hash[:nickname] && name_hash[:realname] && name_hash[:realname][:first] && name_hash[:realname][:last] "#{name_hash[:nickname]} #{name_hash[:realname][:first]} #{name_hash[:realname][:last]}" elsif name_hash[:first] && name_hash[:last] "#{name_hash[:first]} #{name_hash[:last]}" else 'New User' end end

Sekarang, mari kita lihat seperti apa pencocokan pola:

 def display_name(name_hash) case name_hash in {username: username} username in {nickname: nickname, realname: {first: first, last: last}} "#{nickname} #{first} #{last}" in {first: first, last: last} "#{first} #{last}" else 'New User' end end

Preferensi sintaks bisa sedikit subjektif, tetapi saya lebih suka versi pencocokan pola. Ini karena pencocokan pola memungkinkan kita untuk menulis hash yang kita harapkan, alih-alih mendeskripsikan dan memeriksa nilai hash. Ini membuatnya lebih mudah untuk memvisualisasikan data apa yang diharapkan:

 `{nickname: nickname, realname: {first: first, last: last}}`

Dari pada:

 `name_hash[:nickname] && name_hash[:realname] && name_hash[:realname][:first] && name_hash[:realname][:last]`.

Deconstruct dan Deconstruct_keys

Ada dua metode khusus baru yang diperkenalkan di Ruby 2.7: deconstruct dan deconstruct_keys . Ketika sebuah instance dari kelas sedang dicocokkan dengan array atau hash, deconstruct atau deconstruct_keys dipanggil, masing-masing.

Hasil dari metode ini akan digunakan untuk mencocokkan dengan pola. Berikut ini contohnya:

 class Coordinate attr_accessor :x, :y def initialize(x, y) @x = x @y = y end def deconstruct [@x, @y] end def deconstruct_key {x: @x, y: @y} end end

Kode mendefinisikan kelas yang disebut Coordinate . Ini memiliki x dan y sebagai atributnya. Ini juga memiliki metode deconstruct dan deconstruct_keys yang ditentukan.

 c = Coordinates.new(32,50) case c in [a,b] pa #=> 32 pb #=> 50 end

Di sini, sebuah instance dari Coordinate sedang didefinisikan dan pola dicocokkan dengan sebuah array. Apa yang terjadi di sini adalah bahwa Coordinate#deconstruct dipanggil dan hasilnya digunakan untuk mencocokkan dengan array [a,b] yang didefinisikan dalam pola.

 case c in {x:, y:} px #=> 32 py #=> 50 end

Dalam contoh ini, instance Coordinate yang sama sedang dicocokkan pola dengan hash. Dalam hal ini, hasil Coordinate#deconstruct_keys digunakan untuk mencocokkan dengan hash {x: x, y: y} yang ditentukan dalam pola.

Fitur Eksperimental yang Menyenangkan

Setelah pertama kali mengalami pencocokan pola di Elixir, saya pikir fitur ini mungkin menyertakan metode yang berlebihan dan diimplementasikan dengan sintaks yang hanya membutuhkan satu baris. Namun, Ruby bukanlah bahasa yang dibangun dengan pola pencocokan dalam pikiran, jadi ini bisa dimengerti.

Menggunakan pernyataan kasus mungkin merupakan cara yang sangat ramping untuk mengimplementasikan ini dan juga tidak mempengaruhi kode yang ada (selain dari metode deconstruct dan deconstruct_keys ). Penggunaan pernyataan kasus sebenarnya mirip dengan penerapan pencocokan pola Scala.

Secara pribadi, saya pikir pencocokan pola adalah fitur baru yang menarik bagi pengembang Ruby. Ini memiliki potensi untuk membuat kode jauh lebih bersih dan membuat Ruby terasa sedikit lebih modern dan menarik. Saya ingin melihat apa yang orang-orang lakukan tentang ini dan bagaimana fitur ini berkembang di masa depan.