一系列可能性:Ruby 模式匹配指南

已發表: 2022-03-11

模式匹配是 Ruby 2.7 的一大新特性。 它已提交到主幹,因此任何有興趣的人都可以安裝 Ruby 2.7.0-dev 並檢查一下。 請記住,這些都沒有最終確定,開發團隊正在尋找反饋,所以如果你有任何反饋,你可以在功能真正推出之前讓提交者知道。

我希望您在閱讀本文後能夠理解什麼是模式匹配以及如何在 Ruby 中使用它。

什麼是模式匹配?

模式匹配是函數式編程語言中常見的一種特性。 根據 Scala 文檔,模式匹配是“一種根據模式檢查值的機制。 成功的匹配也可以將一個值解構為其組成部分。”

這不應與正則表達式、字符串匹配或模式識別相混淆。 模式匹配與字符串無關,而是數據結構。 我第一次遇到模式匹配是在兩年前我試用 Elixir 的時候。 我正在學習 Elixir 並嘗試用它解決算法。 我將我的解決方案與其他人進行了比較,發現他們使用了模式匹配,這使得他們的代碼更加簡潔易讀。

正因為如此,模式匹配給我留下了深刻的印象。 這就是 Elixir 中的模式匹配:

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

上面的例子看起來很像 Ruby 中的多重賦值。 然而,它不止於此。 它還檢查值是否匹配:

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

在上面的示例中,左側的數字 42 不是正在分配的變量。 這是一個檢查該特定索引中的相同元素是否與右側的元素匹配的值。

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

在此示例中,不是分配值,而是引發MatchError 。 這是因為數字 88 與數字 42 不匹配。

它也適用於地圖(類似於 Ruby 中的哈希):

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

上面的例子檢查鍵name的值是Zote ,並將鍵title的值綁定到變量名上。

當數據結構複雜時,這個概念非常有效。 您可以在一行中分配變量並檢查值或類型。

此外,它還允許像 Elixir 這樣的動態類型語言具有方法重載:

 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

根據參數散列的鍵,執行不同的方法。

希望這向您展示了模式匹配的強大功能。 有許多嘗試使用 noaidi、qo 和 egison-ruby 等 gem 將模式匹配引入 Ruby。

Ruby 2.7 也有自己的實現,與這些 gem 沒有太大的不同,這就是目前的做法。

Ruby 模式匹配語法

Ruby 中的模式匹配是通過case語句完成的。 但是,不是使用通常的when ,而是使用關鍵字in 。 它還支持使用ifunless語句:

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

Case 語句可以接受變量或表達式,這將與in子句中提供的模式匹配。 Ifunless語句也可以在模式之後提供。 這裡的相等性檢查也像正常的 case 語句一樣使用=== 。 這意味著您可以匹配類的子集和實例。 這是您如何使用它的示例:

匹配數組

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

在上面的示例中,變量translation與兩種模式匹配:

['th', orig_text, 'en', trans_text]['th', orig_text, 'ja', trans_text] 。 它的作用是檢查模式中的值是否與每個索引中的translation變量中的值匹配。 如果值匹配,它將translation變量中的值分配給每個索引中模式中的變量。

Ruby 模式匹配動畫:匹配數組

匹配哈希

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

在上面的示例中, translation變量現在是一個散列。 它與in子句中的另一個哈希匹配。 發生的情況是 case 語句檢查模式中的所有鍵是否與translation變量中的鍵匹配。 它還檢查每個鍵的所有值是否匹配。 然後它將值分配給散列中的變量。

Ruby 模式匹配動畫:匹配數組

匹配子集

模式匹配中使用的質量檢查遵循===的邏輯。

多種模式

  • | 可用於為一個塊定義多個模式。
 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

在上面的示例中, translation變量與{orig_lang: 'th', trans_lang: 'ja', orig_txt: orig_txt, trans_txt: trans_txt}哈希和['th', orig_text, 'ja', trans_text]大批。

當您有稍微不同類型的數據結構代表相同的事物並且您希望兩個數據結構執行相同的代碼塊時,這很有用。

箭頭分配

在這種情況下, =>可用於將匹配值分配給變量。

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

當您要檢查數據結構內的值但又要將這些值綁定到變量時,這很有用。

引腳運算符

在這裡, pin 運算符防止變量被重新分配。

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

在上面的示例中,模式中的變量 a 與 1、2 和 2 匹配。它將被分配給 1,然後是 2,然後是 2。如果您想檢查所有的數組中的值相同。

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

當使用 pin 運算符時,它會評估變量而不是重新分配它。 在上面的例子中,[1,2,2] 不匹配 [a,^a,^a] 因為在第一個索引中,a 被賦值為 1。在第二個和第三個中,a 被評估為 1,但與 2 匹配。

但是 [a,b,^b] 匹配 [1,2,2] 因為 a 在第一個索引中分配給 1,b 在第二個索引中分配給 2,然後 ^b,現在是 2,匹配2 在第三個索引中,所以它通過了。

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

也可以使用來自 case 語句之外的變量,如上例所示。

下劃線 ( _ ) 運算符

下劃線 ( _ ) 用於忽略值。 讓我們看幾個例子:

 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

在上面的兩個示例中,任何與_匹配的值都會通過。 在第二個 case 語句中, =>運算符也捕獲已被忽略的值。

Ruby 中模式匹配的用例

假設您有以下 JSON 數據:

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

在您的 Ruby 項目中,您希望解析此數據並使用以下條件顯示名稱:

  1. 如果用戶名存在,則返回用戶名。
  2. 如果暱稱、名字和姓氏存在,則返回暱稱、名字和姓氏。
  3. 如果暱稱不存在,但名字和姓氏存在,則返回名字和姓氏。
  4. 如果條件都不適用,則返回“新用戶”。

這就是我現在用 Ruby 編寫這個程序的方式:

 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

現在,讓我們看看模式匹配的樣子:

 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

語法偏好可能有點主觀,但我更喜歡模式匹配版本。 這是因為模式匹配允許我們寫出我們期望的散列,而不是描述和檢查散列的值。 這使得更容易可視化預期的數據:

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

代替:

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

解構和解構鍵

Ruby 2.7 中引入了兩種新的特殊方法: deconstructdeconstruct_keys 。 當一個類的實例與一個數組或哈希匹配時,分別調用deconstructdeconstruct_keys

這些方法的結果將用於匹配模式。 這是一個例子:

 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

該代碼定義了一個名為Coordinate的類。 它具有 x 和 y 作為其屬性。 它還定義了deconstructdeconstruct_keys方法。

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

在這裡,定義了一個Coordinate的實例,並與一個數組進行模式匹配。 這裡發生的是調用Coordinate#deconstruct並且結果用於匹配模式中定義的數組[a,b]

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

在此示例中,同一個Coordinate實例正在與哈希進行模式匹配。 在這種情況下, Coordinate#deconstruct_keys結果用於匹配模式中定義的哈希{x: x, y: y}

令人興奮的實驗功能

在 Elixir 中第一次體驗模式匹配時,我曾認為此功能可能包括方法重載並使用僅需要一行的語法實現。 但是,Ruby 並不是一種在構建時就考慮到模式匹配的語言,所以這是可以理解的。

使用 case 語句可能是一種非常精簡的實現方式,並且不會影響現有代碼(除了deconstructdeconstruct_keys方法)。 case 語句的使用其實和 Scala 的模式匹配實現類似。

就個人而言,我認為模式匹配對於 Ruby 開發人員來說是一個令人興奮的新特性。 它有可能使代碼更簡潔,讓 Ruby 感覺更現代、更令人興奮。 我很想看看人們對此有何看法,以及該功能在未來如何發展。