1. 程式人生 > >Rust 陰陽謎題,及純基於程式碼的分析與化簡

Rust 陰陽謎題,及純基於程式碼的分析與化簡

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 優化),所以記憶體應該還是會緩慢( O(\sqrt {\text{Len}}) )增長的。

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