在函数式编程社区有一个笑话。任何Scala程序都可以通过组合traverse 函数的正确次数来编写。这篇博文就是专门针对这个笑话的。
在Rust中,Iterator 特质定义了一个特定类型的值流。许多常见的类型提供了一个Iterator 接口。而内置的for 循环结构直接与Iterator trait一起工作。利用这一点,我们可以很容易地做一些事情,比如 "打印一个Vec 中的所有数字":
fn main() {
let myvec: Vec<i32> = vec![1, 2, 3, 4, 5];
for num in myvec {
println!("{}", num);
}
}
假设我们想做一些不同的事情:把Vec 中的每一个值都翻倍。 在Rust中,最习以为常和最有效的方法是使用可变的引用,例如:
fn main() {
let mut myvec: Vec<i32> = vec![1, 2, 3, 4, 5];
for num in &mut myvec {
*num *= 2;
}
println!("{:?}", myvec);
}
既然我们要把这篇文章献给函数式程序员,那么值得注意的是:这看起来绝对不是函数式的。"取一个集合并在每个值上应用一个函数 "在FP圈子里被理解为一个map ,而且在非FP圈子里也越来越被理解为一个Functor 。或者用更多类别理论的术语来说,这是一个map 。幸运的是,Rust为Iterators提供了一个 方法。不幸的是,与Scala或Haskell不同,map 对数据类型不适用,比如Vec 。让我们来比较一下,使用Haskell。
list :: [Int]
list = [1, 2, 3, 4, 5]
main :: IO ()
main = do
let newList :: [Int]
newList = map (* 2) list
print newList
Functor 类型中的map 函数直接作用于一个 list。它产生一个新的列表,并对每个值应用该函数。让我们尝试在 Rust 中做最等效的事情。
fn main() {
let myvec: Vec<i32> = vec![1, 2, 3, 4, 5];
let new_vec: Vec<i32> = myvec.map(|x| x * 2);
println!("{:?}", new_vec);
}
这失败了,出现了错误信息。
no method named `map` found for struct `std::vec::Vec<i32>` in the current scope
这是因为,在Rust中,map 适用于Iterator 本身,而不是底层数据结构。为了在一个Vec ,我们必须使用map 。
- 将
Vec转换为Iterator - 在
map上进行操作。Iterator - 将
Iterator转换回为一个Vec
(1)可以使用IntoIterator 特质来执行,它提供了一个名为into_iter 的方法。对于(3),我们可以编写自己的for 循环,以填充一个Vec 。但正确的方法是使用FromIterator 特质。而最简单的方法是使用Iterator 上的collect 方法来做到这一点。
使用FromIterator和collect
让我们写一个正确使用map 的程序。
fn main() {
let myvec: Vec<i32> = vec![1, 2, 3, 4, 5];
let new_vec: Vec<i32> = myvec
.into_iter()
.map(|x| x * 2)
.collect();
println!("{:?}", new_vec);
}
相当直接,我们的3个步骤变成了3个连锁的方法调用。不幸的是,在实践中,使用collect 往往并不像这样直截了当。这是因为类型推理的原因。为了理解我的意思,让我们把上面的程序中所有的类型注释拿出来。
fn main() {
let myvec = vec![1, 2, 3, 4, 5];
let new_vec = myvec
.into_iter()
.map(|x| x * 2)
.collect();
println!("{:?}", new_vec);
}
这就给了我们一个非常友好的信息。
error[E0282]: type annotations needed
--> src\main.rs:4:9
|
4 | let new_vec = myvec
| ^^^^^^^ consider giving `new_vec` a type
这里的问题是,我们不知道我们应该使用FromIterator 的哪个实现。这是一个在纯FP世界中不存在的问题,即map 和Functor 。在那个世界中,Functor'smap 总是 "保形 "的。当你在Haskell中对一个列表进行map ,结果总是一个列表。
IntoIterator/FromIterator 的组合却不是这样。IntoIterator 破坏了原来的数据结构,完全消耗了它并产生了一个Iterator 。同样地,FromIterator 凭空产生了一个全新的数据结构,没有对原来的数据结构进行任何引用。因此,一个明确的类型注释说明输出的类型是必要的。在我们上面的程序中,我们通过注释new_vec 来做到这一点。另一种方法是使用 "turbofish "来注释要使用的collect 。
fn main() {
let myvec = vec![1, 2, 3, 4, 5];
let new_vec = myvec
.into_iter()
.map(|x| x * 2)
.collect::<Vec<_>>();
println!("{:?}", new_vec);
}
注意,我们只需要指出我们正在收集到一个Vec 。Rust的正常类型推理能够弄清楚。
- 用哪种数字类型来表示这些值
- 原始的
myvec应该是一个Vec,因为它是由vec!宏产生的。
副作用和遍历
好了,我想向全世界宣布,我将把这些数值翻倍。在Rust中修改我们的map-使用的代码来做到这一点很容易。
fn main() {
let myvec = vec![1, 2, 3, 4, 5];
let new_vec = myvec
.into_iter()
.map(|x| {
println!("About to double {}", x);
x * 2
})
.collect::<Vec<_>>();
println!("{:?}", new_vec);
}
但哈斯克人会警告你,这不是那么简单的。map 在哈斯克中是一个纯函数,意味着它不允许有任何副作用(比如打印到屏幕上)。你可以相当容易地看到这个动作。
list :: [Int]
list = [1, 2, 3, 4, 5]
main :: IO ()
main = do
let newList :: [Int]
newList =
map
(\x -> do
putStrLn ("About to double " ++ show x)
pure (x * 2))
list
print newList
这段代码不会被编译,因为Int (一个纯数字)和IO Int (一个有副作用的动作,产生一个Int )之间不匹配。
Couldn't match type 'IO Int' with 'Int'
Expected type: [Int]
Actual type: [IO Int]
相反,我们需要使用map 的更强大的表亲,traverse (又称mapM ,或 "单体地图")。traverse 允许我们执行一系列的动作,并产生一个包含所有结果的新列表。这看起来像:
list :: [Int]
list = [1, 2, 3, 4, 5]
main :: IO ()
main = do
newList <-
traverse
(\x -> do
putStrLn ("About to double " ++ show x)
pure (x * 2))
list
print newList
那么为什么Haskell和Rust在这里有区别呢?这是因为Rust不是一种纯语言。任何函数都可以执行副作用,比如打印到屏幕上。而Haskell则不允许这样做,因此我们需要像traverse 这样的特殊辅助函数来说明潜在的副作用。
我不会去讨论这两种语言之间的哲学差异。我只想说,这两种方法都有其优点,而且都有优点和缺点。让我们来看看Rust的方法在哪里 "失效",以及FromIterator 是怎么做的。
处理失败
在上面Haskell的例子中,我们通过IO 类型使用了副作用。然而,traverse 并不限于与IO 。它可以与许多不同的类型一起工作,任何被认为是Applicative 。而这涵盖了许多不同的普通需求,包括错误处理。例如,我们可以改变我们的程序,不允许将大于5的 "大 "数字加倍。
list :: [Int]
list = [1, 2, 3, 4, 5, 6]
main :: IO ()
main = do
let newList =
traverse
(\x ->
if x > 5
then Left "Not allowed to double big numbers"
else Right (x * 2))
list
case newList of
Left err -> putStrLn err
Right newList' -> print newList
Either 是一个和类型,就像Rust中的 。它等同于Rust中的 ,但名称不同。取代了 和 ,我们有 (按惯例用于成功)和 (按惯例用于失败)。用于它的 实例将在遇到第一个 时停止处理。所以我们上面的程序最终会产生输出 。你可以在 中的 后面放任意多的值,它都会产生相同的输出。事实上,它甚至不会检查这些数字。enum Result Ok Err Right Left Applicative Left Not allowed to double big numbers list 6
回到Rust,让我们首先简单地把我们所有的Results集合到一个Vec ,以确保基本的工作。
fn main() {
let myvec = vec![1, 2, 3, 4, 5, 6];
let new_vec: Vec<Result<i32, &str>> = myvec
.into_iter()
.map(|x| {
if x > 5 {
Err("Not allowed to double big numbers")
} else {
Ok(x * 2)
}
})
.collect();
println!("{:?}", new_vec);
}
这是有道理的。我们已经看到,.collect() 可以把一个Iterator's stream中的所有值塞进一个Vec 。而map 方法现在正在生成Result<i32, &str> 的值,所以所有东西都是一致的。
但这并不是我们想要的行为。我们希望有两个变化:
new_vec应该产生一个 。换句话说,它应该产生一个单一的 ,或者一个成功结果的向量。现在,它有一个成功或失败的值的向量。Result<Vec<i32>, &str>Err- 一旦我们看到一个太大的值,我们应该立即停止处理原始
Vec。
为了更清楚一些,用一个for 循环来实现这一点是很容易的。
fn main() {
let myvec = vec![1, 2, 3, 4, 5];
let mut new_vec = Vec::new();
for x in myvec {
if x > 5 {
println!("Not allowed to double big numbers");
return;
} else {
new_vec.push(x);
}
}
println!("{:?}", new_vec);
}
但是现在我们已经完全失去了我们的map ,我们正在下降到使用显式循环、变异和短路(通过return )。换句话说,这段代码让我觉得不那么优雅了。
事实证明,我们原来的代码几乎是完美的。让我们来看看一点神奇的东西,然后解释一下它是如何发生的。我们之前的代码版本使用了map ,结果是Vec<Result<i32, &str>> 。而我们想要Result<Vec<i32>, &str> 。如果我们简单地把类型改成我们想要的,会发生什么?
fn main() {
let myvec = vec![1, 2, 3, 4, 5, 6];
let new_vec: Result<Vec<i32>, &str> = myvec
.into_iter()
.map(|x| {
if x > 5 {
Err("Not allowed to double big numbers")
} else {
Ok(x * 2)
}
})
.collect();
match new_vec {
Ok(new_vec) => println!("{:?}", new_vec),
Err(e) => println!("{}", e),
}
}
多亏了FromIterator 的力量,这才得以实现!为了了解原因,让我们看看关于FromIterator 的一些文档。
获取
Iterator中的每个元素:如果它是一个Err,就不再获取其他元素,并返回Err。如果没有Err,则会返回一个包含每个Result的值的容器。
突然间,似乎Rust一直都在实现traverse!FromIterator 设置中的这种额外的灵活性使我们能够重新获得FP人所熟悉的traverse 中的短路错误处理行为。
与traverse 相比,我们仍然在处理两个不同的特征(IntoIterator 和FromIterator ),而且没有任何东西可以阻止这些特征成为不同的类型。因此,某种类型的注解仍然是必要的。一方面,这可以被看作是Rust方法的一个弊端。另一方面,它允许我们更灵活地生成什么类型,我们将在下一节中讨论这个问题。
最后,事实证明,我们可以用turbofish来再次拯救我们。比如说:
fn main() {
let myvec = vec![1, 2, 3, 4, 5, 6];
let new_vec = myvec
.into_iter()
.map(|x| {
if x > 5 {
Err("Not allowed to double big numbers")
} else {
Ok(x * 2)
}
})
.collect::<Result<Vec<_>, _>>();
match new_vec {
Ok(new_vec) => println!("{:?}", new_vec),
Err(e) => println!("{}", e),
}
}
不同的FromIterator impls
到目前为止,我们只看到了FromIterator 的两种实现:Vec 和Result 。还有很多可用的。我最喜欢的一个是HashMap ,它可以让你把一连串的键/值对收集成一个映射。
use std::collections::HashMap;
fn main() {
let people = vec![
("Alice", 30),
("Bob", 35),
("Charlies", 25),
].into_iter().collect::<HashMap<_, _>>();
println!("Alice is: {:?}", people.get("Alice"));
}
由于FromIterator impl forResult 的工作方式,你可以将这两者叠加起来,将一串Result的对收集到一个Result<HashMap<_, _>, _> 。
use std::collections::HashMap;
fn main() {
let people = vec![
Ok(("Alice", 30)),
Ok(("Bob", 35)),
Err("Uh-oh, this didn't work!"),
Ok(("Charlies", 25)),
].into_iter().collect::<Result<HashMap<_, _>, &str>>();
match people {
Err(e) => println!("Error occurred: {}", e),
Ok(people) => {
println!("Alice is: {:?}", people.get("Alice"));
}
}
}
验证
在Haskell世界中,我们有两个不同的错误收集概念。
Either一个是 "在第一个错误时停止",另一个是 "收集所有的错误"。Validation意思是 "把所有的错误收集在一起"。
Validation 对于解析Web表单这样的事情来说是非常有用的。你不想只生成第一个失败,而是把所有的失败收集在一起,以产生更友好的用户体验。为了好玩,我决定在Rust中也实现这个功能。
我很想用Rust写一个Validation "Applicative",用一个FromIterator implator来收集多个Err值。我没有真正的需要,但它似乎仍然很有趣。
- Michael Snoyman (@snoyberg)2020年10月1日
正如你从该主题的其余部分所看到的,这篇博文的很多动机来自Twitter的回复。
在Rust中的实现是相当直接的,而且相当容易理解。我已经把它放在Github上了。如果有兴趣将其作为一个板块,请在问题追踪器中告诉我。
为了看到它的作用,让我们修改上面的程序。首先,我将在我的Cargo.toml 文件中添加该依赖关系。
[dependencies.validation]
git = "https://github.com/snoyberg/validation-rs"
rev = "0a7521f7022262bb00aea61761f76c3dd5ccefb5"
然后修改代码,使用Validation 枚举而不是Result 。
use std::collections::HashMap;
use validation::Validation;
fn main() {
let people = vec![
Ok(("Alice", 30)),
Ok(("Bob", 35)),
Err("Uh-oh, this didn't work!"),
Ok(("Charlies", 25)),
Err("And neither did this!"),
].into_iter().collect::<Validation<HashMap<_, _>, Vec<&str>>>();
match people.into_result() {
Err(errs) => {
println!("Errors:");
errs.into_iter().map(|x| println!("{}", x)).collect()
}
Ok(people) => {
println!("Alice is: {:?}", people.get("Alice"));
}
}
}
奖金注意有点厚颜无耻地使用map 和collect 来打印出错误。这是在利用FromIterator 的() 隐含值,它将一系列() 的值收集在一起,成为一个单一的值。
总结
我意识到这是一个有点漫无边际的旅程,但希望对Rustaceans、Haskellers和Scala人来说是一个有趣的旅程。以下是我的一些收获:
collect方法是非常灵活的- 在
collect,没有任何魔法,只有FromIterator特质和实现它的类型的行为。- 这对我来说是一个很大的收获。几个月前,我不知不觉地忘记了
FromIterator,并对collect的 "秘密 "行为感到紧张。
- 这对我来说是一个很大的收获。几个月前,我不知不觉地忘记了
collect的缺点是,由于它不像map或traverse那样保存结构,所以有时你需要类型注释。- 习惯于使用turbofish!
- 有很多有用的
FromIterator的impls。