编译器前端的胡思乱想

date
Jun 11, 2023
slug
status
Published
tags
Compiler
type
Post
summary
一致与便捷,泛化与具象,“度”是最考验一个人的审美和架构能力的。

主题一:一致性和便捷性往往会冲突,因为想便捷往往会走例外,这样就会破坏一致性。

 
情况一:if/while等流程语句下面有两个选择:单行不带花括号/多行带花括号,前者无疑是便捷的,同时也是符合直觉的,因为花括号是代码块,而代码块本质上是可以看作一句语句的。但是这样会导致一些类似Dangling else的混淆;换个思路的话,如果我们去泛化和抽象,定义if等语句下面就是跟代码块,而单行只是代码块的一个具体的情况,这样的话强制代码块又合情合理了。 这两种想法的本质不同是,单个语句和代码块究竟哪个是泛化的根,哪个是具象的叶。久经纠结之后我选择了强制加花括号,原因有两点:
  1. 首先如果认为代码块是一条语句的具象(即一种特殊的语句)的话,那么语句后面理应加分号,这样的话代码块的花括号之后应该加上分号,而这无疑是便捷性的一种损失。但是反过来想单个语句是代码块的一种具象(即只有单句的代码块)的话,那么代码块里是语句加分号合情合理;
  1. 另外从实践上看单句代码虽然便捷,但是加入分割的花括号无疑能够使程序阅读起来更加清晰,能够减少很多歧义例如:if(x>0); printf(”xxx”); 和 Dangling else 这些问题,以上例子还不仅仅是阅读复杂的问题,还是风险问题(固然阅读复杂也有风险,但不会有歧义,同时容易被聚焦到),尤其是后者依赖于编译器的实现,这在我看来是不可原谅的,虽然这么说可能从真理问题偏向了口味问题,但是最终我决定取消单行语句不加花括号的设计。
 
情况二:是否每行语句末尾都要加分号。不加分号固然便捷,例如go、python、rust等语言都是单行语句末尾不加分号。但是我在这个问题上可以说没有丝毫犹豫,选择强制加分号,原因有两点:
  1. 我的设计原则是,绝对不能让排版影响程序的运行,比如说我进行一下压缩,将所有代码都压缩到一行,这样就必须要加入分号分隔了。(顺便说一下我觉得python用错进来控制代码真的好sb)。
  1. 其二,可加可不加的操作,极其破坏一致性,极其破坏选择恐惧症程序员的编码,当面临选择是我要么选择加,这样就和我的想法一致了,反正都选了加,那我直接编译器里面加入一条限制也无所谓了;要么选择不加,但是这样的话如果我想一行写两句的话又得被迫加上,这样一个文件有的有加号有的没加号,ttmd恶心心了。不好意思这里口味问题了,但是也有一个偏真理的结论:less is more,不影响功能情况下以及不(中上)影响体验下,让用户少做选择。前面半句是go语言的原则,后面的是我瞎扯的:)。
 
 

主题二:泛化与具象之间需要有一个限度,而这个限度需要通过热点数据来衡量。泛化有助于理解,但过于的泛化会导致混乱和不stable的感觉。

情况一:函数的声明与定义,首先我们定义变量是这样的 var x:i32 = 10;那么问题就来了,如果将函数的定义泛化到极致的话,大概长这样:fn test : (x:i32, y:f64) : (i32, i32)={…},我们来分析一下这里使用冒号作为类型说明,第一个冒号后面的(x:i32, y:f64):(i32, i32)定义了test的原型,即test的类型是(x:i32, y:f64):(i32, i32),而第二个红色背景的冒号则是说明test : (x:i32, y:f64)的类型是(i32, i32),ok这样理解的话让冒号充分发挥了一致性,但是这样也带来了很多问题:
  1. 首先,将函数泛化到这个程度,函数和变量的声明格式可以认为是一致的了,这样前面的fn就没有了意义。
  1. 另外,如果这个函数没有返回那么我们是否还要写第二个冒号呢,如果省略的话就变成了这样 fn test : (x:i32, y:f64)={…},那么这就产生了混淆test的类型就变成了元组,另外如果函数没有输入只有输出的话我们又该如何去定义呢,我想了很久想要完美的解决这个问题,我们只能把它写成如下形式 fn test:():()={…},这是一个没有输入输出的函数的例子,可以看到要想不产生歧义我们就需要保留两个括号,二者无疑使便捷性和可阅读行大打折扣。
所以我就开始了思考,为什么会带了这些问题,就是因为我们把函数类泛化到和变量定义一致的层次,那么就需要在辨别时引入更多特征,这很难用语言来描述,不过这种思想类似于KMP算法,就是如果我们把函数和变量看作一个东西,假如说叫A,那么对于每个a我们的初始信息量是平等的,但是如果我们把函数看作B,把变量看作C,那么我们初始就已经得到了一些信息,对于b、c的特征描述肯定就可以产生不同,同时相对于a我们的信息肯定更多(因为引入了交集之外的信息)
所以我觉得这个问题的设计可以从函数类的特征出发,函数包含输入和输出以及定义。所以最终我的选择是如下形式: fn test (x:i32, y:f64)->(m:i32, n:i32) {},首先取消了冒号,原因有二:
  1. 箭头能够更加清晰的表明映射关系,而冒号和元组中的冒号交相辉映很容易混乱。比如:fn test x:i32:y:f64 {}
  1. 冒号的有优点是包含着类型定义的意思,但是缺点也在于此,如果只有第二个冒号,那么就存在可能让用户联想到是否加上第一个冒号才是圆满的句号,以及联想到是否应该加上等于号,这种感觉就是分手了还留有遗憾,所以我的建议是分手了就拉黑,避免触物思人,既然打破一致性了那就直接让他不一致,这本身也在维护一致性。
另外我们要介绍元组的特点了,元组 组完 有序而不可变,这一特性就支持很重要一点:元组基数为1时自动退化为元素。接下来就可以大战身手了,可以有以下多种情况:
/* 多输入,多输出。 *(这里需要注意的是花括号是必须的,不允许单条语句就不加花括号, * 加了花括号就不用加分号了,因为主题一中已经介绍了单条语句为代码块的具象, * 而且这里要强制花括号后面不允许有分号,一致性完美保持,嘿嘿🤭) * 假如这里使用冒号就变成了这样:fn test:(x:i32, y:i32):(m:i32, n:i32) = {}*/ fn test (x:i32, y:i32) -> (m:i32, n:i32) {} /* 多输入,单输出。 * 需要注意的是这里的小括号在单参数时可以去掉, * 因为这与元组的定义是符合的,单元素和单元素元组是等价的, * 这样就不算损害一致性了。 * 假如这里使用冒号就变成了这样:fn test:(x:i32, y:i32):m:i32 = {}*/ fn test (x:i32, y:i32) -> m:i32 {} /* 多输入,无输出 * 假如这里使用冒号就变成了这样:fn test:(x:i32, y:i32) = {}*/ fn test (x:i32, y:i32) {} /* 单输入,多输出 * 假如这里使用冒号就变成了这样:fn test:x:i32:(m:i32, n:i32) = {}*/ fn test x:i32 -> (m:i32, n:i32) {} /* 单输入,单输出 * 假如这里使用冒号就变成了这样:fn test:x:i32:m:i32 = {}*/ fn test x:i32 -> m:i32 {} /* 单输入,无输出 * 假如这里使用冒号就变成了这样:fn test:x:i32 = {}*/ fn test x:i32 {} /* 无输入,多输出 * 假如这里使用冒号就变成了这样:fn test::(m:i32, n:i32) = {} * 注意这里为了多输入,无输出一定要多加一个冒号,这就违反了一致性 *(除非都加两个冒号,即多输入无输出变为 fn test:(x:i32, y:i32): = {}),还很混乱*/ fn test -> (m:i32, n:i32) {} /* 无输入,单输出 * 假如这里使用冒号就变成了这样:fn test::m:i32 = {} * 这里和上一条一样被迫加入两个连续的冒号*/ fn test -> m:i32 {} /* 无输入,无输出 * 假如这里使用冒号就变成了这样:fn test = {} * 从和变量对比的层次来说这就相当于自动类型推导了*/ fn test {}
其实通过以上的例子,可以看出还有一个很重要的属性是使用冒号会使代码阅读性降为地狱,那就是输入和输出的形参都可以匿名,这种情况下举个例子,定义无输入单输出会变成这样 fn test::i32 = {} 以及定义单输入无输出可能是这样的fn test:i32: = {},为了防止和定义变量混淆所以又在末尾加了一个冒号,总之要想函数类和变量类看作一种对象,那么冒号和等于号都不可或缺,而这些东西的最初目的是为了让用户快速理解,结果效果却南辕北辙了。总结一下,“冒号法“想要达到真正的一致性需要写成如下形式 fn test:: = {},冒号一个不能少。
既然说到了匿名参数,那么我么就来分析一下上述方案中匿名参数的玩法(当然最常用的玩法还是输入不匿名,输出匿名):
//多输出匿名,操作比较常规,注意这里有一些代码省略当不影响思路的展现。 fn test (x:i32, y:i32) -> (i32, i32) {return m, n;} (t1, t2) = test(x, y) //单输出匿名,由于基数为1的元组就是元素本身所以结构时t1不需要小括号 fn test (x:i32, y:i32) -> i32 {return m, n;} t1 = test(x, y) //多输出不匿名,操作就是新鲜的了,但符合直觉。 fn test (x:i32, y:i32) -> (m:i32, n:i32) {return 1, 2;} m = test(x, y).m //单输入不匿名,新鲜的地方与上面一致 m = test(x, y).m //多输入匿名,操作也有点新鲜,借鉴了shell脚本解构参数的思想,@0是函数名类型为String。 fn add (i32, i32) -> i32 {return @1 + @2;}
前面提到了匿名参数,还可以再说一下匿名函数,其实很简单只要去掉函数名就可以了,其它一点都不影响,如下:
/* 这里就又产生歧义了,花括号后面要不要加分号呢,等号后面是代码块这样看可以不加, * 但是整体看是一条语句,总不能再往这个单条语句外面加花括号,这不合理,但是加分号也不合理*/ fn the_fn = fn (i32, i32) -> i32 {return @1 + @2;} var z:i32 = the_fn(4, 6); //or var z:i32 = fn (i32, i32) -> i32 {return @1 + @2;}(5, 5);
这种功能性值得假如一个冒号,虽然前面说了冒号要加就都加,避免”睹物思人“,但是换个想法对于函数来说,函数是个什么类型更重要的是第一个冒号后面的整体,前面我们首先考虑的是保留第二个冒号,去思考去不去第一个冒号,这其实是一个思维盲区,因为第二个冒号的信息对于函数来说是第一个冒号后面信息的子集,所以深思熟虑之后,我决定改革,改成刚刚提到的,保留第一个冒号。这样的话匿名函数的使用就变了,变成如下形式:
var z:i32 = fn (i32, i32) -> i32 {return @1 + @2;}(5, 5); //闭包玩法 fn parent -> fn -> i32 { var i:i32 = 1; return fn -> i32 {i++;}; }; fn x = parent(); y = x(); //y = 1 y = x(); //y = 2
 
WeChat Pay
If you have any questions, please contact me.
WeChat Pay