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
Rust 阴阳谜题,及纯基于代码的分析与化简的更多相关文章
- 基于纯Java代码的Spring容器和Web容器零配置的思考和实现(3) - 使用配置
经过<基于纯Java代码的Spring容器和Web容器零配置的思考和实现(1) - 数据源与事务管理>和<基于纯Java代码的Spring容器和Web容器零配置的思考和实现(2) - ...
- scheme 阴阳谜题
本篇分析continuation的一个著名例子"阴阳迷题",这是由David Madore先生提出的,原谜题如下: (let* ((yin ((lambda (foo) (disp ...
- 纯javascript代码编写计算器程序
今天来分享一下用纯javascript代码编写的一个计算器程序,很多行业都能用到这个程序,例如做装修预算.贷款利率等等. 首先来看一下完成后的效果: 具体代码如下:(关注我的博客,及时获取最新WEB前 ...
- Android 使用纯Java代码布局
java布局 java代码布局和xml布局的区别 1.Java纯布局更加的灵活,比如自定义控件或一些特殊要求时,使用java代码布局 2.常用的xml布局是所见即所得的编写方式,以及xml本身拥有一些 ...
- DataX通过纯Java代码启动
DataX是阿里巴巴团队开发的一个很好开源项目,但是他们对如何使用只提供了python命令启动方式,这种方式对于只是想简单的用下DataX的人来说很是友好,仅仅需要几行代码就可以运行,但是如果你需要在 ...
- 帧动画的创建方式 - 纯Java代码方式
废话不多说,先看东西 帧动画的创建方式主要以下2种: * 用xml创建动画: * 纯Java代码创建动画: 本文内容主要关注 纯java代码创建帧动画 的方式: 用xml创建帧动画:http:// ...
- Entity Framework入门教程(18)---EF6中基于代码进行配置方式
EF6中基于代码进行配置方式 我们以前对EF进行配置时是在app.config/web.config下的<entityframework>节点下进行配置的,EF6引进了基于代码的配置方法. ...
- 20.2.翻译系列:EF 6中基于代码的数据库迁移技术【EF 6 Code-First系列】
原文链接:https://www.entityframeworktutorial.net/code-first/code-based-migration-in-code-first.aspx EF 6 ...
- 教程 | 如何使用纯NumPy代码从头实现简单的卷积神经网络
Building Convolutional Neural Network using NumPy from Scratch https://www.linkedin.com/pulse/buildi ...
随机推荐
- AbelSu区块链应用场景收集
1.基于智能合约的众筹 众筹项目的资金通常由一个中心化不可变更且开放的数据库来控制,这个数据库可以追踪所有出资人. 虽然如此,我们可以用一种去中心化的方式来实现,而且只要创建一个代币就可以追踪资金.一 ...
- BZOJ4999: This Problem Is Too Simple!树链剖分+动态开点线段树
题目大意:将某个节点的颜色变为x,查询i,j路径上多少个颜色为x的点... 其实最开始一看就是主席树+树状数组+DFS序...但是过不去...MLE+TLE BY FCWWW 其实树剖裸的一批...只 ...
- 大数据入门第十四天——Hbase详解(一)入门与安装配置
一.概述 1.什么是Hbase 根据官网:https://hbase.apache.org/ Apache HBase™ is the Hadoop database, a distributed, ...
- 20155331《网络对抗》Exp5 MSF基础应用
20155331<网络对抗>Exp5 MSF基础应用 基础问题回答 用自己的话解释什么是exploit,payload,encode 答:exploit就是漏洞利用.exploit就是利用 ...
- Android开发——Fragment知识整理(二)
0. 前言 Android开发中的Fragment的应用非常广泛,在Android开发--Fragment知识整理(一)中简单介绍了关于Fragment的生命周期,常用API,回退栈的应用等知识.这 ...
- 【HNOI2017】礼物
题面 题解 显然两个手环只需要一个的亮度增加\(c \in [-m, m]\)和原题是等价的. 于是可以写成这样一个公式: \[ \sum_{i = 1} ^ n(x_i - y_{i+k} + c) ...
- 洛咕 P3756 [CQOI2017]老C的方块
四染色,贼好想 一个弃疗图形刚好对应一个红-绿-黄-粉色路线(不要吐槽颜色) 就是裸的最小割,建图傻逼懒得写了 #include<bits/stdc++.h> #define il inl ...
- python3 简单进度条代码
进度条代码函数实现 import sys, time class ShowProcess(object): """ 显示处理进度的类 调用该类相关函数即可实现处理进度的显 ...
- MyBatis最初的程序解读---API
API详解: * 线程安全问题出现的条件 (1) 只有单例对象才可能出现线程安全问题 (2) 多线程环境,即多个线程会共享这个单例对象 ...
- UE4中Timeline的使用
UE4中经常需要一些和时间相联系的功能,例如在一段时间内完成一个动作,播放一段动画,或者只是单纯的延迟函数的执行时间,即调整事件的执行顺序.在UE4的蓝图自带函数中有一个很好用的函数可以完美地解决这些 ...