本文概述
模式匹配是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
上面的示例检查键名的值是否为Zote, 并将键名的值绑定到变量标题。
当数据结构复杂时, 此概念非常有效。你可以一行分配变量并检查所有值或类型。
此外, 它还允许像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之类的Ruby中。
Ruby 2.7的实现也与这些gem没有太大的区别, 这就是目前的做法。
Ruby模式匹配语法
Ruby中的模式匹配是通过case语句完成的。但是, 不是使用通常的时间, 而是使用关键字in。它还支持使用if或else语句:
case [variable or expression]
in [pattern]
...
in [pattern] if [expression]
...
else
...
end
Case语句可以接受变量或表达式, 并将其与in子句中提供的模式匹配。在模式之后也可以提供if或not语句。此处的相等性检查也像常规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
在上面的示例中, 变量转换针对两种模式进行匹配:
[‘th’, orig_text, ‘en’, trans_text]和[‘th’, orig_text, ‘ja’, trans_text]。它的作用是检查模式中的值是否与每个索引中的转换变量中的值匹配。如果值确实匹配, 则会将转换变量中的值分配给每个索引中模式中的变量。
匹配的哈希
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
在上面的示例中, 转换变量现在是哈希。它与in子句中的另一个哈希匹配。发生的情况是case语句检查模式中的所有键是否与转换变量中的键匹配。它还检查每个键的所有值是否匹配。然后, 将值分配给哈希中的变量。
匹配子集
模式匹配中使用的质量检查遵循===的逻辑。
多种模式
- |可用于为一个块定义多个模式。
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
在上面的示例中, 转换变量与{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
当你要检查数据结构内部的值并将这些值绑定到变量时, 这很有用。
引脚运算符
此处, 引脚运算符可防止变量被重新分配。
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项目中, 你想解析此数据并使用以下条件显示名称:
- 如果用户名存在, 则返回用户名。
- 如果存在昵称, 名字和姓氏, 请返回昵称, 名字和姓氏。
- 如果昵称不存在, 但名字和姓氏存在, 则返回名字, 然后返回姓氏。
- 如果没有任何条件适用, 请返回”新用户”。
这就是我现在用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]`.
解构和Deconstruct_keys
Ruby 2.7中引入了两个新的特殊方法:deconstruct和deconstruct_keys。当将某个类的实例与数组或哈希匹配时, 分别调用deconstruct或deconstruct_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作为其属性。它还定义了deconstruct和deconstruct_keys方法。
c = Coordinates.new(32, 50)
case c
in [a, b]
p a #=> 32
p b #=> 50
end
在这里, 定义了一个Coordinate实例, 并针对一个数组进行了模式匹配。这里发生的是调用Coordinate#deconstruct并将结果用于与模式中定义的数组[a, b]匹配。
case c
in {x:, y:}
p x #=> 32
p y #=> 50
end
在此示例中, 正在将同一个Coordinate实例与哈希进行模式匹配。在这种情况下, Coordinate#deconstruct_keys结果用于与模式中定义的哈希{x:x, y:y}进行匹配。
令人兴奋的实验功能
在Elixir中首次经历过模式匹配之后, 我以为该功能可能包括方法重载, 并使用仅需要一行的语法来实现。但是, Ruby不是一种基于模式匹配而构建的语言, 因此这是可以理解的。
使用case语句可能是实现此目的的一种非常精简的方法, 并且也不会影响现有代码(除deconstruct和deconstruct_keys方法之外)。 case语句的使用实际上与Scala的模式匹配实现类似。
就个人而言, 我认为模式匹配对于Ruby开发人员而言是令人兴奋的新功能。它有可能使代码更加整洁, 并使Ruby更具现代感和令人兴奋。我很乐意看到人们对此的理解以及该功能在将来的发展。
评论前必须登录!
注册