【译】Rust模式匹配入门指南

799 阅读9分钟

原文标题:Pattern matching in Rust and other imperative languages
原文链接:doma-dev.medium.com/pattern-mat…
公众号: Rust 碎碎念
翻译 by: Praying

太长不看版

Rust

  • Rust 是一门拥有与模式相关(pattern-related )的语言设施最多的命令式语言(imperative language);

  • Rust 既有浅解构(shallow destructuring)也有深解构(deep destructuring);

  • if let匹配形式可以用原来缓解 multiple-head 的缺陷。

JavaScript

  • JavaScript 有很多模式相关的语言特性;

  • array 基于位置的解构和 object 基于 key 的析构;

  • Rest 参数,解构支持;

  • 浅拷贝操作;

  • 来自 Microsoft,Facebook 和 NPM 的支持,在 JS 中进行适当的模式匹配是不可避免的。

Python 和 C++

  • 在语言层面对模式匹配的初步支持;

  • 支持packing/unpacking;

  • C++有很强大的库支持模式匹配,语言层面的支持可能会在 C++23 中实现。





一直以来,编程语言理论研究和函数式编程世界里的思想和方法不断被筛选并进入传统编程语言之中,现在,就连 Excel 都支持 lambda 了。

在本文中,我们将会涵盖各种命令式编程语言中的模式匹配。我们会帮助你采用模式匹配来增强代码的表达力和简洁性。

C++演化提案中的一个示例

Rust 中的模式匹配

在所有的命令式语言之中,Rust 拥有最为高级且设计良好的模式系统。当然,一部分原因在于 Rust 开发者能够从头开始构建一门语言。但更为重要的是,它源于设计开发的严谨和文化。

Rust 中的模式匹配几乎有着和 Haskell 中一样丰富的功能。为了了解这些功能,首先,让我们考虑下面的任务(受到真实用例启发):

探索一个非严格结构的 JSON 对象,其中 key 是物种,value 是该物种对应的动物集合。

如果一个动物具备毛皮或者羽毛,它是Cute,否则,它是Weird。如果一个物种是"aye-aye",它是Endangered。以后可能还会发现新的判断标准用以改变某一特定动物或物种的分类。

在给定的数据集中将具有独特的name的动物进行分类!

下面,让我们来编码实现分类:

#[derive(Hash, Debug, PartialEq, Eq, PartialOrd, Ord)] /* A */
pub enum Category {
  Cute,
  Weird,
  Endangered,
}

(A)确保了 Rust 将会从上到下对 value 进行排序,所以Cute < Weird < Endangered 这个排序在后面会非常重要。

现在来编写任务中的规则。因为我们的 JSON 是非结构的(数据),我们不能依赖任何现有的属性,所以我们不能安全地unwrap或者可靠地把 JSON 强制转换为某种 Rust 数据结构:

fn cat_species(v: &str) -> Category {
  match v {
    "aye-aye" => Category::Endangered, /* A */
    _ => Category::Cute, /* B */
  }
}

我们的第一个match!多么令人激动!当然,这个 match 操作相当于对变量v的内容进行 witch。但是,它会在后面提供更多的灵活性。借助于解构,我们可以匹配更复杂的结构体,而不仅仅是单个变量。

上面的A中,展示了如果匹配一个字面值(literal value),(B)展示了"catch-all"语句(译注:即捕获所有)。这个模式匹配认为名为“aye-aye”的物种是危险的(endangered),其他物种是可爱的(cute)。

现在,让我们看一下如何去写一些更为有趣的东西:

fn cat_animal_first_attempt(v: &Value) -> Category {
  match v["coat"].as_str() {
    Some("fur") | Some("feathers") => Category::Cute,
    _ => Category::Weird,
  }
}

上面的代码实现了判断可爱的规则,没有使用 unwrap,也没有显式地检查这个值是否有一些内容(Some)或是什么也没有(None)!这份代码描述了:具有毛皮(fur)或羽毛(feather)表面的动物是可爱的(cute),其他的都是怪异的(weird)。

但是,这个实现足够好么?我们可以考虑添加新的规则来检查一下,正如需求中提醒我们的那样:

具有白化突变(albino mutation)的动物是Endangered。否则,应用之前的规则判断。

fn cat_animal_first_attempt_1(v: &Value) -> Category {
  let cat = match v["coat"].as_str() { /* A */
    Some("fur") | Some("feathers") => Category::Cute, /* B */
    _ => Category::Weird,
  }
  match v["mutation"].as_str() {
    Some("albino") => Category::Endangered,
    _ => cat
  }
}

这段代码变得臃肿且刻板,我们现在必须在A中插入某个变量。我们必须要记住,不能因为不小心加了一个return从而使得(B)中的计算短路(short-circuit )。一旦突然出现一个额外的规则,我们需要在可变的cat和版本化(versioned)之间做出决定。

所以,当我们需要捕获异构的匹配集时,模式匹配就会崩溃?也不尽然。让我们引入if let语句,它的出现就是为了应对这种挑战:

fn cat_animal(v: &Value) -> Category {
  if let Some("albino") = v["mutation"].as_str() {
    Category::Endangered
  } else if let Some("fur")
              | Some("feathers")
              = v["coat"].as_str() {
    Category::Cute
  } else {
    Category::Weird
  }

现在就更像样了。但是等一下,这意味着什么?和其他的模式匹配一样,左手边是一个模式(例如,Some("albino")),右手边是值(例如,v["mutation"].as_str())。当且仅当左手边的模式匹配右手边的值,if后面的分支才会被执行。

使用了if let语法的模式匹配,使我们从最具体的子句开始,按照明确的顺序落到不太具体的子句,消除了过多的自由度,从而使代码不易出错。

汇总

pub fn categorise(
  data: HashMap<String, Vec<Value>>,
) -> HashMap<Category, Vec<String>> {
  let mut retval = HashMap::new();
  for (species, animals) in data {
    for animal in animals {

      if let Some(name) = (animal["name"].as_str()) { /* A */
        retval
          .entry(max(cat_species(species.as_str()),
                     cat_animal(&animal))) /* B */
          .or_insert(Vec::new()) /* C */
          .push(name.to_string())
      }

    }
  }
  retval
}

现在我们已经有了分类函数,我们可以继续对我们的数据集进行分类。如果(A)中的if let匹配失败(当前的动物没有名字),我们将会进入到下一轮迭代。不是所有的模式都必须有 catch-all 分支。

此外,变量name将会存储当前动物的名字,并且我们将会通过一个便利的HashMapAPI 来链接某些函数。在(B)中,我们使用Category 枚举(enum)的Ord实例,通过std::cmp::max函数来确定基于物种的分类和按动物分类之间的最高优先级类别。

HashMapentry返回类别下的值的引用。如果是 None,C中的or_insert会插入一个空的 vector 并返回它的引用。最后,我们可以把当前动物的名字放入到这个 vector 之中,并且这个名字将会出现在我们的映射(mapping)中。

我们希望本文能对 Rust 中的模式匹配进行合理的介绍。示例的完整代码详见sourcehut

让我们以其他命令式语言中的模式相关特性的一些信息来结束本文。

现代 JavaScript 中的模式

const foldAndDump = (path, xs, ...cutoffs) => {
  // snip
  for (c of cutoffs) {
    //snap
  }
}

ECMAScript 的一个老功能,JS 标准中被称为"rest parameters",...cutoffs将会把一个函数参数中第二个以后的参数匹配进一个称为cutoffs的 array 中。

var rs = [];
for (let [printing, info] of
     Object.entries(allPrintingsJson['data']))
{
    rs.push({ ...info, "_pv_set": printing });
}

当省略不是参数列表时,它意味着我们正在处理一个更新的特性,被称为“展开语法(spred syntax)”。...info意味着按原样包含 info 对象。类似的,展开语法可以将一个可枚举的对象分散到一个函数调用的参数中。

const xs = [1,2,3];
console.log(sum(...xs));

最后,是 unpacking,现在这是一个相当漂亮的特性:

> [a,b] = [1,2]
[ 1, 2 ]
> {x,y} = {y: a, x: b}
{ y: 1, x: 2 }
> {k,l} = {y: a, x: b}
{ y: 1, x: 2 }
> [a,b,x,y,k,l]
[ 1, 2, 2, 1, undefined, undefined ]

Python 中的 packing 和 unpacking

在现代 Python 中,所有可迭代的(变量)都是 unpackable 的:

>>> a, *b, c = {'hello': 'world', 4: 2, 'rest': True, False: False}
>>> a, b, c ('hello', [4, 'rest'], False)

*类似于 JS 中的省略(...)操作符。它可以收集“剩下的值”,也可以以迭代的方式展开:

>>> print(*[1, 2, 3])
1 2 3

相反地,受到 Python 启发,还有一种被称为“字典拆分操作符”的特殊操作符。它的工作方式非常类似于展开操作符。

>>> print({'x': True, **{'y': False}, **{'x': False, 'z': True}}) {'x': False, 'y': False, 'z': True}

准备一下:即将进入模式匹配

每种处于活跃开发中的语言都在尝试从函数式语言中吸收越来越多的特性,模式匹配这个特性也不例外。

我么将会以一个编程语言列表来结束本文,列表中的编程语言不同程度地实现了模式匹配,根据对其适配的确定性程度进行排序。

Python 中的模式匹配

  • 在 Python 的历史上有过若干不同的提案,但最终PEP 634得到了实现。

  • 实现了“结构化模式匹配”的 Python 的 Alpha 版本在2021 年 3 月 1 日就可以获取了。

C++中的模式匹配

  • C++中的模式匹配,正如在演化文档所见,可能在 C++23 中落地实现。

  • 在你等待新标准的时候,也有一两个库做了类似新标准的实现。

JavaScript 中的模式匹配

  • 并列 "最有可能恰当采用模式匹配 "的第一名,被称为“ECMAScript”的 JavaScript 标准中有这个提案,该提案由 Microsoft,Facebook 和 NPM 支持。

  • 该提案经过全面审查,被移至 "第一阶段",这使得该功能的理论发布时间在 2023-2025 年。

  • 你可以通过检查已完成的提案库中的 git 日志来检查我们的计算结果。

模式匹配的思想是基于模式而不是条件的代码执行分支。使用模式匹配的程序员不是试图编码一个代码分支得到执行所必需的值的属性,而是编码在事件发生时值应该是怎样的。因此,在命令式语言中,与ifcase等谓词语句相比,模式匹配提供了更多的表达性和声明性代码,除了一些边角情况。

这其中的差异很微妙,但一旦你理解了,你的工具库中就会增加一个非常强大的表达方式。

我们发现,理解这些概念就像理解声明式与命令式编程范式一样。对于那些对哲学感兴趣的人,我们建议找一个舒适的夜晚,蜷缩在一起,喝上一杯热气腾腾的饮料,观看 Kevlin Henney 的 declarative thinking, declarative practice的视频分享。