一系列可能性: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 感觉更现代、更令人兴奋。 我很想看看人们对此有何看法,以及该功能在未来如何发展。