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 ...
随机推荐
- ASP.NET Core中,UseDeveloperExceptionPage扩展方法会吃掉异常
在ASP.NET Core中Startup类的Configure方法中,有一个扩展方法叫UseDeveloperExceptionPage,如下所示: // This method gets call ...
- excel实用技巧——vlookup函数
1.VLOOKUP函数的套路 VLOOKUP(要找谁,在哪儿找,返回第几列的内容,精确找还是近似找) 最后一个参数: 如果为0或FASLE,用精确匹配方式,而且支持无序查找: 如果为TRUE或被省略, ...
- Django Rest Framework源码剖析(三)-----频率控制
一.简介 承接上篇文章Django Rest Framework源码剖析(二)-----权限,当服务的接口被频繁调用,导致资源紧张怎么办呢?当然或许有很多解决办法,比如:负载均衡.提高服务器配置.通过 ...
- vue eslint报错解决办法
若提示入下图时,在build / webpack.base.conf.js中, 找到 // const createLintingRule = () => ({// test: /\.(js|v ...
- PowerBI开发 第三篇:报表设计技巧
最近做了几个PowerBI报表,对PowerBI的设计有了更深的理解,对数据的塑形(sharp data),不仅可以在Data Source中实现,例如在TSQL查询脚本中,而且可以在PowerBI中 ...
- jQuery .attr() vs. .prop()
Property vs. Attribute 在开始正式比较prop()和attr()两个jQuery方法之前,我们有必要先弄清一下Property和Attribute两个单词的意思.在中文里面,它们 ...
- ansible自动化工具安装和简单使用
ansible自动化工具安装和简单使用 1.安装 ansible依赖于Python 2.6或更高的版本.paramiko.PyYAML及Jinja2. 2.1 编译安装 解决依赖关系 # yum -y ...
- idea使用actiBPM插件中文乱码
idea 安转activiti插件后,编辑流程图发现保存后中文乱码,并且idea的字符集(Settings—>Editor—>File Encodings)已经设置为UTF-8,流程图中中 ...
- linux内核分析第三周
20135103王海宁 linux内核分析第三周 http://mooc.study.163.com/course/USTC-1000029000 按照课堂提供的方法,命令行一行行敲上去,我是手机缓 ...
- linux 常用命令-用户、用户组管理
注:本篇只涉及常用命令,全部命令可以通过help帮助查看. (1)type useradd #查看命令属于内部命令还是外部命令,内部命令是嵌在linux的shell中,外部命令存储在路径中 (2) ...