Rust 阴阳谜题,及纯基于代码的分析与化简

时间:2024-05-03 00:06:19

Rust 阴阳谜题,及纯基于代码的分析与化简

雾雨魔法店专栏 https://zhuanlan.zhihu.com/marisa

来源 https://zhuanlan.zhihu.com/p/52249705

0. 前(请务必跳过)

之前用 Haskell 通过 Cont Monad 模拟过 call/cc (实际上在阴阳谜题中用作 get-current-continuation,这里我们只讨论 get/cc),但似乎确实是搞个 DSL 再模拟。

但我是觉得这和动态类型其实关系不大,只是通常语言是栈机模型,而 call/cc 的“栈”是一棵树,还可能到处跳。唯一和类型有关的是 get/cc 类型是递归类型 a where a ~ (a -> _|_),但我们可以用类似 data Out a = In (Out a) (Out a) 的实现,在需要的时候把Cont翻成Cont -> Cont,或者反过来即可。

1. Rust 代码实现

因为不想搞得那么学术派,我们不用 Haskell 那种数学语言,用很工程很靠谱的 Rust 实现以下这个 阴阳谜题/YinYang Puzzle。

首先,我们直译一下 :

yin = getcc();
print!("@");
yin = getcc();
print!("*");
yin(yang);

但这当然是搞不了的。

我们 getcc 拿来的 yin不可能在全局都能用(主程序还是栈机啊喂,超级 goto 过分了),我们限定它在一个闭包里面才能用(这里我们要手动 CPS 一下),具体多大范围按需即可。

此外,由于函数调用的重载还没 stable,用了怕一下有 stable 癖的人觉得这不 Rust,所以这里用成员函数实现。

所以我们的代码应该是这样,然后一跑发现已经是预期行为了:(Rust Playground)

/// Continuation.
/// Cont ~ (Cont -> !) We use `()` instead of `!` here since `!` not stable
struct Cont<'a>(&'a dyn Fn(&Cont)); impl Cont<'_> {
fn call(&self, value: &Cont) {
(self.0)(value); // Simple proxy. Note that it is dynamic dispatch.
}
} /// Equal to `{ let cc_ = getcc(); cc(cc_); }`
/// Apparently, `cc_` and `cc` is the same continuation.
fn with_cc(cc: impl Fn(&Cont)) {
cc(&Cont(&cc)); // Call `cc` with `cc` itself (current continuation)
} fn puzzle() {
with_cc(|yin| {
print!("@");
with_cc(|yang| {
print!("*");
yin.call(yang);
});
});
}

输出:

@*@**@***@****@*****@******@*******@********@**** .....stack overflow

PS:惊奇地发现这份代码在 Release 下跑可以避免栈溢出,一直输出下去,看来是 TCO 了,果然优化还是很强劲的。当然记得本地编译跑,在线会被杀掉而看不到输出。

PSS:因为这里闭包引用结构的嵌套无法消去(我觉得 Rust 应该做不了 Idris 的 Nat <=> Int 优化),所以内存应该还是会缓慢( Rust 阴阳谜题,及纯基于代码的分析与化简 )增长的。

2. 分析与化简

现在我们试着只从代码上分析,尽量避免数学推导,证明为何是这样的输出。

(才不是因为看不懂 pi-calculus / 不会分析平行宇宙呢)

首先,我们这里有两个闭包,|yin| { .. }没有捕获东西,|yang| { .. }捕获了上一层的yin的引用,我们要手动展开闭包语法糖。

然后考虑到&dyn Fn(&Cont) 是动态分发,但只可能是两个闭包之一,直接用 enum实现这个 Trait Object 引用,也是展开语法糖。

因为闭包代码都很少,这里我们直接把函数体代码 inline 进动态分发的call里去了。

(Rust Playground)

enum Cont<'a> { // Desugar of `&dyn Fn(&Cont)`
ClosureA,
ClosureB { yin: &'a Cont<'a> },
} impl Cont<'_> {
fn call(&self, value: &Cont) {
match self { // Manually dynamic dispatch
Cont::ClosureA => {
let yin = value;
print!("@");
with_cc(Cont::ClosureB { yin });
}
Cont::ClosureB { yin } => {
let yang = value;
print!("*");
yin.call(yang);
}
}
}
} fn with_cc(cc: Cont) {
cc.call(&cc);
} fn puzzle() {
with_cc(Cont::ClosureA);
}

可能还看不出来调用顺序如何,但call经过或不经过with_cc,最终递归调用自己,至少可以知道它是个死循环,而且似乎还是尾递归的。

然后我们可以发现,这个 enum Cont实际上就是一个不带值的链表结构( Cont::ClosureA <=> Null,Cont::ClosureB <=> Next),它只包含长度信息。

所以我们只用一个自然数即可和它一一对应。

(对,这就是皮亚诺自然数定义的 Nat,但因为不要学术,不展开)

0 <=> Cont::ClosureA
1 <=> Cont::ClosureB { yin: &Cont::ClosureA }
2 <=> Cont::ClosureB { yin: &Cont::ClosureB { yin: &Cont::ClosureA } }
...

我们直接定义 type Cont = usize来重写简化一下call函数。

多套一层就是加一,模式匹配就是判零/减一。

type Cont = usize;

fn call(this: Cont, value: Cont) {
if this == 0 {
let yin = value;
print!("@");
let cc = yin + 1;
call(cc, cc);
} else {
let yin = this - 1;
let yang = value;
print!("*");
call(yin, yang);
}
} fn puzzle() {
call(0, 0);
}

哇,尾递归!就是循环!

然后我们把两个函数 inline 到一起:

Rust Playground 上把死循环改成 for了,不然卡死看不到输出)

fn puzzle() {
let (mut this, mut value) = (0, 0);
loop {
// for _ in 0..1024 { // For test running online
if this == 0 {
print!("@");
this = value + 1;
value = value + 1;
} else {
print!("*");
this = this - 1;
// value = value; // Unchanged
}
}
}

这下可以清楚看到这个拍扁的二重循环结构了:

  1. this == 0 时,value自增 1,并设this = value, 输出一个@
  2. 否则一次this自减 1,输出一个*

最后重写成更语义化的二重循环就好啦:

3. 最终化简代码

(Rust Playground 限制了第一个for范围以防止死循环)

子循环是thisvalue自减到 1,(0 不输出了 *,直接返回上一层了) 。当然显然这个循环顺序其实没啥关系,为了和上面对应还是反过来了。

fn puzzle() {
for value in 1.. { // The value after `print`, starting from 1
// for value in 1..64 { // For test running online
print!("@");
for _this in (1..=value).rev() {
print!("*");
}
}
}

大循环一次一个@,然后小循环输出 value*,自增value,重复。

输出结果当然就是 @*@**@***@****@*****@******@*******@********@....啦 。

================= End