Rust for Rubyists

Matthias Endler

写给 Ruby 开发者的 Rust

原文由 Matthias Endler 发布,订阅该博客

最近我读到了一篇关于地道 Ruby 写法的精彩文章。虽说我绝不是什么出色的 Ruby 开发者,但我发现其中很多模式在 Rust 中也十分常见。接下来将对两种语言的地道代码进行并列对比。

Ruby 代码示例均来自原文

Map 与高阶函数

第一个例子是对容器中的元素进行基本的遍历,使用 map

user_ids = users.map { |user| user.id }

map 这一概念在 Rust 中也同样常见。相比 Ruby,我们需要写得更明确一些:如果 users 是一个存放 User 对象的 vector,我们首先需要从它创建一个迭代器:

let user_ids = users.iter().map(|user| user.id);

你可能会觉得这样写有点啰嗦,但这种额外的抽象让我们能够表达一个重要的概念:迭代器究竟会不会取得 vector 的所有权?

  • 使用 iter() 时,你得到的是对 vector 的“只读视图”。迭代结束后,它保持不变。
  • 使用 into_iter() 时,你会取得 vector 的所有权。迭代结束后,vector 就不存在了。用 Rust 的术语来说,它已经被 moved(移动)了。
  • 在这里阅读更多关于 iter()into_iter() 区别的介绍

上面的 Ruby 代码还可以这样简化:

user_ids = users.map(&:id)

在 Ruby 中,高阶函数(如 map)以块(block)或 proc 作为参数,语言本身为方法调用提供了一个便捷的简写——&:id 就等同于 {|o| o.id()}

在 Rust 中也可以做类似的事情:

let id = |u: &User| u.id;
let user_ids = users.iter().map(id);

不过,这可能不是最地道的写法。更常见的是在这种情况下使用通用函数调用语法(Universal Function Call Syntax)1

let user_ids = users.iter().map(User::id);

在 Rust 中,高阶函数以 函数 作为参数。因此 users.iter().map(Users::id) 在很大程度上等同于 users.iter().map(|u| u.id())2

另外,Rust 中的 map() 返回的是另一个迭代器,而不是集合。如果你想要得到集合,就得像后面会看到的那样,在其上调用collect()

使用 Each 进行迭代

说到迭代,我在 Ruby 代码中经常看到这样的模式:

["Ruby", "Rust", "Python", "Cobol"].each do |lang|
  puts "Hello #{lang}!"
end

Rust 1.21 起,这在 Rust 中也可以实现了:

["Ruby", "Rust", "Python", "Cobol"]
    .iter()
    .for_each(|lang| println!("Hello {lang}!", lang = lang));

不过,在 Rust 中更常见的写法是普通的 for 循环:

for lang in ["Ruby", "Rust", "Python", "Cobol"].iter() {
    println!("Hello {lang}!", lang = lang);
}

Select 与 filter

假设你想从一个集合中筛选出所有的偶数,在 Ruby 中可以这样写。

even_numbers = [1, 2, 3, 4, 5].map { |element| element if element.even? } # [ni, 2, nil, 4, nil]
even_numbers = even_numbers.compact # [2, 4]

在这个例子中,调用 compact 之前,我们的 even_numbers 数组里包含了 nil 条目。而在 Rust 中,并没有 nilNull 的概念。你不需要 compact。而且 map 也不接受谓词(predicate)。你应该为此使用 filter

let even_numbers = vec![1, 2, 3, 4, 5]
    .iter()
    .filter(|&element| element % 2 == 0);

或者,把结果构造成一个 vector

// Result: [2, 4]
let even_numbers: Vec<i64> = vec![1, 2, 3, 4, 5]
    .into_iter()
    .filter(|element| element % 2 == 0).collect();

一些提示:

  • 我在那里使用了类型标注 Vec<i64>,因为如果没有它,Rust 在调用 collect 时就不知道我想构建哪种集合。
  • vec! 是用于创建 vector 的宏。
  • 我在这里用的是 into_iter 而不是 iter。这样我就能取得 vector 中元素的所有权。如果用 iter(),得到的会是 Vec<&i64>

在 Rust 中,数字并没有 even 方法,但这并不妨碍我们自己定义一个!

let even = |x: &i64| x % 2 == 0;
let even_numbers = vec![1, 2, 3, 4, 5].into_iter().filter(even);

在真实场景中,你可能会使用像 num 这样的第三方包(crate)来处理数值计算:

extern crate num;
use num::Integer;

fn main() {
    let even_numbers: Vec<i64> = vec![1, 2, 3, 4, 5]
        .into_iter()
        .filter(|x| x.is_even()).collect();
}

一般来说,在 Rust 中为标准库之外的功能使用 crate 是非常普遍的。之所以能被广泛接受,部分原因在于 cargo 是一个非常出色的包管理器。(也许是因为它正是由 Ruby 界大名鼎鼎的 Yehuda Katz 打造的。 😉)

如前所述,Rust 没有 nil。不过,操作失败的概念依然存在。用于表达这一概念的规范类型叫做 Result

假设你想把一个字符串 vector 转换成整数。

let maybe_numbers = vec!["1", "2", "nah", "nope", "3"];
let numbers: Vec<_> = maybe_numbers
    .into_iter()
    .map(|i| i.parse::<u64>())
    .collect();

这样写看起来不错,但输出可能有点出乎意料。numbers 中同样会包含解析错误:

[Ok(1), Ok(2), Err(ParseIntError { kind: InvalidDigit }), Err(ParseIntError { kind: InvalidDigit }), Ok(3)]

有时你只关心成功的操作。一个简单过滤掉错误的方法是使用 filter_map

let maybe_numbers = vec!["1", "2", "nah", "nope", "3"];
let numbers: Vec<_> = maybe_numbers
    .into_iter()
    .filter_map(|i| i.parse::<u64>().ok())
    .collect();

我在这里改了两处:

  • 原来用的是 map,现在改成了 filter_map
  • parse 返回的是 Result,但 filter_map 期望的是 Option。我们可以通过在其上调用 ok() 来把 Result 转换成 Option3

返回值包含了所有成功转换的字符串:

[1, 2, 3]

filter_map 类似于 Ruby 中的 select 方法:

[1, 2, 3, 4, 5].select { |element| element.even? }

随机数

在 Ruby 中,从数组中获取一个随机数可以这样写:

[1, 2, 3].sample

这非常简洁,也很地道!来对比一下 Rust 的写法:

let mut rng = thread_rng();
rng.choose(&[1, 2, 3, 4, 5])

要让这段代码正常工作,你需要引入 rand crate。点击代码片段可查看可运行的示例。

与 Ruby 相比有一些不同。也就是说,我们需要更明确地指定究竟想要哪一种随机数生成器。我们选择了一个惰性初始化的线程局部随机数生成器,由系统提供种子。在这个例子中,我使用的是 切片(slice) 而不是 vector。主要区别在于切片的大小是固定的,而 vector 则不是。

在标准库范围内,Rust 并没有在切片本身上提供 samplechoose 方法。这是一个设计上的取舍:语言的核心被保持得尽可能小,以便未来能够持续演进。

这并不意味着你现在就无法拥有更优雅的实现。例如,你可以为 [T] 定义一个 Choose trait 并为其实现:

extern crate rand;
use rand::{thread_rng, Rng};

trait Choose<T> {
    fn choose(&self) -> Option<&T>;
}

impl<T> Choose<T> for [T] {
    fn choose(&self) -> Option<&T> {
        let mut rng = thread_rng();
        rng.choose(&self)
    }
}

这些样板代码可以放到一个 crate 中,供他人复用。这样,我们就能得到一个足以媲美 Ruby 优雅程度的解决方案。

[1, 2, 4, 8, 16, 32].choose()

隐式返回与表达式

Ruby 方法会自动返回最后一条语句的结果。

def get_user_ids(users)
  users.map(&:id)
end

Rust 也是如此。注意缺少的分号。

fn get_user_ids(users: &[User]) -> Vec<u64> {
    users.iter().map(|user| user.id).collect()
}

但在 Rust 中,这仅仅是个开始,因为一切皆为表达式。下面的代码块将字符串拆分为字符,移除其中的 h,并将结果作为 HashSet 返回。这个 HashSet 会被赋值给 x

let x: HashSet<_> = {
    // Get unique chars of a word {'h', 'e', 'l', 'o'}
    let unique = "hello".chars();
    // filter out the 'h'
    unique.filter(|&char| char != 'h').collect()
};

条件判断也是如此:

let x = if 1 > 0 { "absolutely!" } else { "no seriously" };

由于 match 语句同样也是表达式,你也可以将其结果赋值给变量!

enum Unit {
    Meter,
    Yard,
    Angstroem,
    Lightyear,
}

let length_in_meters = match unit {
    Unit::Meter => 1.0,
    Unit::Yard => 0.91,
    Unit::Angstroem => 0.0000000001,
    Unit::Lightyear => 9.461e+15,
};

多重赋值

在 Ruby 中,你可以一步将多个值赋给多个变量:

def values
  [1, 2, 3]
end

one, two, three = values

在 Rust 中,你只能将元组解构为元组,例如,无法将 vector 解构为元组。所以这样写是可以的:

let (one, two, three) = (1, 2, 3);

但这样不行:

let (one, two, three) = [1, 2, 3];
//    ^^^^^^^^^^^^^^^^^ expected array of 3 elements, found tuple

这样也不行:

let (one, two, three) = [1, 2, 3].iter().collect();
// a collection of type `(_, _, _)` cannot be built from an iterator over elements of type `&{integer}`

不过在 nightly 版本的 Rust 中,你现在可以这样写:

let [one, two, three] = [1, 2, 3];

另一方面,通过解构你能做到的远不止多重赋值。你可以利用模式语法编写出优美、符合人体工学的代码。

let x = 4;
let y = false;

match x {
    4 | 5 | 6 if y => println!("yes"),
    _ => println!("no"),
}

这会打印 no,因为 if 条件适用于整个模式 4 | 5 | 6,而不仅仅是最后一个值 6。

字符串插值

Ruby 拥有丰富的字符串插值支持

programming_language = "Ruby"
"#{programming_language} is a beautiful programming language"

可以这样翻译:

let programming_language = "Rust";
format!("{} is also a beautiful programming language", programming_language);

命名参数也是可行的,尽管要少见得多:

println!("{language} is also a beautiful programming language", language="Rust");

Rust 的 println!() 语法甚至比 Ruby 的还要丰富。如果你好奇还能做些什么,可以查阅文档

就到这里!

Ruby 为许多常见的使用模式提供了语法糖,从而能够写出非常优雅的代码。底层编程和极致性能并不是该语言的主要目标。

如果你正好需要这些,Rust 可能是个不错的选择,因为它在提供细粒度的硬件控制的同时,也具备可比拟的人体工学。如果拿不准,Rust 更倾向于显式;它避免使用魔法。

是不是已经对地道的 Rust 心动了?可以看看 这个 GitHub 项目。非常期待你的贡献。

脚注

1. 感谢 Florian Gilcher 的提示。
2. 感谢 masklin 指出了多处不准确之处。
3. 在第一版中,我曾说 ok() 会把 Result 转换成 boolean,这是错误的。感谢 isaacg 的指正。

本文章由 muse-spark-1.2-contributor 进行翻译

评论