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
優化),所以記憶體應該還是會緩慢( )增長的。
2. 分析與化簡
現在我們試著只從程式碼上分析,儘量避免數學推導,證明為何是這樣的輸出。
(才不是因為看不懂 pi-calculus / 不會分析平行宇宙呢)
首先,我們這裡有兩個閉包,|yin| { .. }
沒有捕獲東西,|yang| { .. }
捕獲了上一層的yin
的引用,我們要手動展開閉包語法糖。
然後考慮到&dyn Fn(&Cont)
是動態分發,但只可能是兩個閉包之一,直接用 enum
實現這個 Trait Object 引用,也是展開語法糖。
因為閉包程式碼都很少,這裡我們直接把函式體程式碼 inline 進動態分發的call
裡去了。
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 } } }
這下可以清楚看到這個拍扁的二重迴圈結構了:
this == 0
時,value
自增 1,並設this = value
, 輸出一個@
;- 否則一次
this
自減 1,輸出一個*
;
最後重寫成更語義化的二重迴圈就好啦:
3. 最終化簡程式碼
(Rust Playground 限制了第一個for
範圍以防止死迴圈)
子迴圈是this
從value
自減到 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