#逻辑 #类型 #元编程 #证明

无需std rsmonad

Haskell风格的monads,用于惯用的Rust

7个版本

0.2.4 2023年5月25日
0.2.2 2023年5月23日
0.1.2 2023年5月19日

#241 in 数学

MPL-2.0LGPL-3.0-or-later

74KB
1.5K SLoC

稳定Rust中的Monads、Functors及更多

无宏的Rust语法中的Haskell风格monads。

类型无需零注解

一个示例(编译并作为测试运行在gallery.rs中)

assert_eq!(
    vec![
        || "this string isn't a number",
        || "67",
        || "that was, but not binary",
        || "1010101010",
        || "that was, but more than 8 bits",
        || "101010",
        || "that one should work!",
        || panic!("lazy evaluation!"),
    ]
    .fmap(|x| move || u8::from_str_radix(x(), 2).ok())
    .asum(),
    Some(42)
);

简化后的do语法

assert_eq!(
    list![1..20]
        >> |x| {
            list![{ x }..20]
                >> |y| {
                    list![{ y }..20]
                        >> |z| guard::<List<_>>(x * x + y * y == z * z) >> |_| list![(x, y, z)]
                }
        },
    list![
        // Including "non-primitive" (i.e. multiples of smaller) triples
        (3, 4, 5),
        (5, 12, 13),
        (6, 8, 10),
        (8, 15, 17),
        (9, 12, 15)
    ]
);

具有Rust向量速度的Haskell列表逻辑

use rsmonad::prelude::*;
let li = list![1, 2, 3, 4, 5];
fn and_ten(x: u8) -> List<u8> { list![x, 10 * x] }
assert_eq!(li >> and_ten, list![1, 10, 2, 20, 3, 30, 4, 40, 5, 50]);

无类型注解的N-fold bind

// from the wonderful Haskell docs: https://wikibooks.cn/wiki/Haskell/Understanding_monads/List
fn bunny(s: &str) -> List<&str> {
    list![s, s, s]
}
assert_eq!(
    list!["bunny"] >> bunny,
    list!["bunny", "bunny", "bunny"],
);
assert_eq!(
    list!["bunny"] >> bunny >> bunny,
    list!["bunny", "bunny", "bunny", "bunny", "bunny", "bunny", "bunny", "bunny", "bunny"],
);

甚至不需要类型注解的著名棘手的join-in-terms-of-bind

let li = list![list![0_u8]]; // List<List<u8>>
let joined = li.join();      // -->  List<u8>!
assert_eq!(joined, list![0]);

语法糖

  • Rust要求>>=是自修改的,所以我们使用>>代替。return(关键字)变为consume,而Haskell的>>变为&
  • 函数应用顺序已标准化,以匹配>>=而不是<$>:它是x.fmap(f)表示f <$> x
    • 我认为这使它更容易作为一个计算链阅读,但我不坚持这一点,类型系统两种方式都适用。
  • 对于functors,你可以使用fmap(f, x)x.fmap(f),或者你可以管道化它:x % f % g % ...
  • Applicative 使用 * 而不是 <*>(或显式的 tie 方法)。
  • Alternative 使用 | 而不是 <|>(或显式的 either 方法)。
  • Monoids 使用 + 而不是 <>

使用

只需写下 monad! { ...,你就可以免费获得其所有超类(FunctorApplicativeAlternative、...),以及基于属性的 monad 定律测试。

use rsmonad::prelude::*;

enum Maybe<A> {
    Just(A),
    Nothing,
}

monad! {
    Maybe<A>:

    fn consume(a) {
        Just(a)
    }

    fn bind(self, f) {
        match self {
            Just(a) => f(a),
            Nothing => Nothing,
        }
    }
}

// And these just work:

// Monad
assert_eq(Just(4) >> |x| u8::checked_add(x, 1).into(), Just(5));
assert_eq(Nothing >> |x| u8::checked_add(x, 1).into(), Nothing);
assert_eq(Just(255) >> |x| u8::checked_add(x, 1).into(), Nothing);

// Functor
assert_eq!(Just(4) | u8::is_power_of_two, Just(true));
assert_eq!(Nothing | u8::is_power_of_two, Nothing);

Rust 特定的 monads

无需担心细节即可捕获 panic

fn afraid_of_circles(x: u8) -> BlastDoor<()> {
    if x == 0 { panic!("aaaaaa!"); }
    Phew(())
}
assert_eq!(
    Phew(42) >> afraid_of_circles,
    Phew(())
);
assert_eq!(
    Phew(0) >> afraid_of_circles,
    Kaboom,
);

锋利边缘

目前,当你有一个类似于 MaybeMonad 的具体实例时,你只能使用 >> 来进行 bind 操作。对于一般形式的 <M: Monad<A>>,后者仍然可以工作,但需要显式调用 m.bind(f)(或者如果你没有使用 trait,Monad::<A>::bind(m, f))。这应该在 Rust 的非生命周期绑定功能推出时得到解决。

在 doctest 中出现“无法在此作用域中找到类型 ...

Doctests 试图猜测 fn main { ... } 的位置,如果你没有提供,有时它将 rsmonad 宏读作应该放在 main 块中的内容。尝试在你想运行的语句周围添加一个显式的 fn main() { ... }。如果你不想 fn main() { ... } 显示在文档中但无法修复此错误,请将其注释掉。

/// monad! { ... }
/// # fn main {
/// a();
/// b();
/// c();
/// # }

#![no_std]

禁用默认功能

# Cargo.toml

[dependencies]
rsmonad = { version = "*", default-features = false }

请注意,这也会禁用 List,尽管这可能是你想要的:我们 无法 在编译时知道它的长度(这就是它的 bind 实现的意义),因此它需要一个堆。正在开发一个用于 #![no_std] extern crate alloc;alloc 功能,但它尚未完成。

依赖关系

~1.5–2.3MB
~41K SLoC