2 个版本
使用旧版 Rust 2015
0.1.1 | 2018 年 11 月 8 日 |
---|---|
0.1.0 | 2016 年 7 月 16 日 |
#1076 在 Rust 模式
25 每月下载量
在 sidekiq-rs 中使用
11KB
69 行
hado
通过 hado!
宏将 Haskell 类似表达式引入 Rust
是什么?
一个小型宏,用于编写 Haskell 类似的 do 表达式,无需太多仪式感
为什么?
Rust 在错误处理方面非常明确(通过 Option 和 Result 类型),但处理所有错误可能会很繁琐,因此此库引入了来自 Haskell 类似语言的组合 monad 模式。
让我们看看一个例子:我们将尝试进行简单的数学运算并组合可能出现的错误。
首先我们定义我们的返回类型,此类型将代表函数的成功或失败。
type MathResult = Option<f64>;
- 除法:如果除数为 0,则失败
fn div(x: f64, y: f64) -> MathResult {
if y == 0.0 {
None
} else {
Some(x / y)
}
}
- 平方根:如果得到负数,则失败
fn sqrt(x: f64) -> MathResult {
if x < 0.0 {
None
} else {
Some(x.sqrt())
}
}
- 对数:再次,如果得到负数,则失败
fn ln(x: f64) -> MathResult {
if x < 0.0 {
None
} else {
Some(x.ln())
}
}
现在我们想获取两个数字,将它们相除,然后获取结果的平方根,最后获取最后一个结果的对数
直接的方式
fn op(x: f64, y: f64) -> MathResult {
let ratio = div(x, y);
if ratio == None {
return None
};
let ln = ln(ratio.unwrap());
if ln == None {
return None
};
return sqrt(ln.unwrap())
}
尽管这段代码可以工作,但它很难扩展,并且不是 Rust 的惯用语法,它看起来更像是 Java 中的代码,其中 None
是 NULL
。
更好的方式
fn op(x: f64, y: f64) -> MathResult {
match div(x, y) {
None => None,
Ok(ratio) => match ln(ratio) {
None => None,
Ok(ln) => sqrt(ln),
},
}
}
这个例子更接近 Rust,但它仍然看起来有很多噪音,而且仍然很难扩展
函数式编程的方式
fn op(x: f64, y: f64) -> MathResult {
div(x, y).and_then(|ratio|
ln(ratio).and_then(|ln|
sqrt(ln)))
}
这种方式看起来几乎像是我们要做的特殊事情,但那些 and_then
和闭包似乎是不必要的
hado 宏的方式
fn op(x: f64, y: f64) -> MathResult {
hado! {
ratio <- div(x, y);
ln <- ln(ratio);
sqrt(ln)
}
}
这里我们有一个非常明显的方式来声明我们的意图,没有任何错误处理的迹象,我们只需要添加一个 trait Monad
到 Option(该库中默认已定义)
错误类型无关
现在一些更高级的功能,你可能认为 but what about the try! macro, 它肯定会使事情变得更好, right?
,我的回答是 yes,但 try 宏 仅 在 Result
类型上工作,因此无法更改错误类型(或将其与基于 Option 的函数一起使用)。
但现在我们需要知道导致计算失败的错误是什么。因此,我们将 MathResult
别名更改为 f64 或自定义类型 MathError
的 Result
#[derive(Debug)]
pub enum MathError {
DivisionByZero,
NegativeLogarithm,
NegativeSquareRoot,
}
type MathResult = Result<f64, MathError>;
因此,现在我们需要更改每个函数,因为现在所有的 None
和 Some
构造函数都是类型错误
例如,div 将变为
fn div(x: f64, y: f64) -> MathResult {
if y == 0.0 {
Err(MathError::DivisionByZero)
} else {
Ok(x / y)
}
}
注意,唯一的变化是
无Err(MathError::DivisionByZero)
一些Ok (x / y)
现在我们来检查每个实现的 op 函数
- 朴素方法:更改所有构造函数、失败检查器,幸运的是 Rust 的类型推断帮助我们避免了更改太多类型声明。
- 更好的方法:稍微好一些,构造函数相同,但失败检查器被 match 语句替换,但仍然非常冗长。
- 函数式编程方法:要好得多,我们唯一担心的是新错误类型有某种
and_then,其他所有东西都应该正常工作。 - hado 方法:与上一个类似,但现在没有需要更改的内容,计算没有对失败框架的关注。
如何使用它?
该宏可以根据在 crate 内部定义的 trait(默认实现为 Option 和 Result)执行一些基本操作
让我们从 Rust 书和 Rust by Example 中取一些示例,并将它们转换为 hado 格式。
Result 类型参考示例
这是原始的基于 try 的错误处理,使用早期返回
fn write_info(info: &str) -> io::Result<()> {
let mut file = try!(File::create("my_best_friends.txt"));
println!("file created");
try!(file.write_all(format!("rating: {}\n", info.rating).as_bytes()));
Ok(())
}
以下是基于 hado 的
fn hado_write_info(string: &str) -> io::Result<()> {
hado!{
mut file <- File::create("my_best_friends.txt");
println!("file created");
file.write_all(format!("string: {}\n", string).as_bytes())
}
}
注意 ign 关键字是特殊的,它表示丢弃内部值,但在失败的情况下,整个表达式将短路到该错误。由于没有箭头(<-
),所以 println
的返回值被完全丢弃,因此可以在此处有任何非失败语句(包括 let
和 use
)
多参数构造函数
假设我们有一个 Foo 结构体,它有一个新方法
fn new(a: i32, b: f64, s: String) -> Foo {
Foo {
a: i32,
b: f64,
s: String
}
}
从普通构造函数创建一个 Option 构造函数,无需太多麻烦
fn opt_new(a: Option<i32>, b: Option<f64>, s: Option<String>) -> Option<Foo> {
a <- a;
b <- b;
s <- s;
ret new(a, b, s)
}
注意,仅通过更改类型签名,就可以将 Option 更改为任何其他单调类型。
您还可以在不进行大量样板操作的情况下进行一些自定义验证
fn opt_new(a: i32, b: f64, s: String) -> Option<Foo> {
a <- validate_a(a);
b <- validate_b(b);
s <- validate_s(s);
ret new(a, b, s)
}