|
|
51CTO旗下网站
|
|
移动端
创建专栏

当你在纠结学Python还是Java时,大二学生已经开始造编程语言了!

我在之前的文章中说过多次,大学生在校期间应该去搞定那些基础知识,因为大学生没有工作的压力,有大块的时间,基础知识相对枯燥,要想有成就感,可以做一些简单的系统,例如一个简单的编程语言,一个有基本功能的OS......

作者:杨韬|2019-02-22 14:25

 导读

我在之前的文章中说过多次,大学生在校期间应该去搞定那些基础知识,因为大学生没有工作的压力,有大块的时间,基础知识相对枯燥,要想有成就感,可以做一些简单的系统,例如一个简单的编程语言,一个有基本功能的OS......

杨韬是我的知识星球“码农翻身”的一个大学生,他在星球提到做了一个简单的编程语言解释器,我建议他把过程给写出来, 就是这篇文章了。

下文的“我”就是杨韬。

为什么要自己写一个解释器?

从大学开始学习编程, 现在已经快两年了, 接触了不少的编程语言。最开始入门学了C语言; 后来想写安卓软件, 学了Java; 接着接触后台开发,学了Python; 后面又陆续地接触Go, Dart, C++。 仔细算算, 已经接触过6门语言了!

但是仔细想想我似乎又什么也没有学到,过年回家的时候, 遇到一个对计算机很感兴趣的四年级的小朋友(ps: 小朋友会写一点点python, html, 现在的小孩也太强了), 问了我诸如计算机上面的程序是怎么跑起来的, 代码是怎么运行这种看似很基础的问题, 可悲的是我居然对这些问题似懂非懂, 不能给小朋友很清楚地解释出来。

我都不好意思说自己是学计算机的了, 居然连这些基础的问题都没有搞清楚。 这是促使我去深入学习编译原理, 计算机组成原理和操作系统这些基础知识的重要原因。

学习编译原理最简单的方法(对我来说)大概是自己实现一门编程语言, 虽然费时费力, 不过能对整个过程有个清晰的了解。

另外一个重要原因是有一种想要自己写一门语言的冲动。 尤其是在学了这么多门语言之后就会萌生出这样的念头。不同的语言有不同的让我喜欢的特性: Python有漂亮简洁的语法, 静态语言Go实现了像动态语言一样的鸭子类型的接口, Dart有很多语法糖和方便的异步… (当然这些特性是仁者见仁, 智者见智的)。

但是又不能找到一门语言,具备所有自己喜欢的特性, 那就自己写一个好了,可以把自己喜欢的特性都加上。

学习的过程

学校的编译原理的课程安排在了大三, 我还没有学过, 所以一切都是从0开始。 我先看了前桥和弥写的《自制编程语言》一书, 这本书的实战性很强, 没有介绍太多的理论知识, 而是直接教你怎么写编程语言。

我从这本书中了解了写一门编程语言的大致过程和大致的思路。 不过书中的很多解释不够充分, 对于完全没有接触过编译技术的人来说还是有点费解(也可能是我自己理解能力不够好)。

我也是在自己了解了大致思路后就开始自己尝试写, 然后再回过头来看书, 看作者提供的源代码, 才能比较好地了解作者是在干嘛。

还有一些书中内容介绍的不够充分, 比如yacc和lex的使用。 这种工具毕竟比较流行, 网上找找别人写的博客多看看, 自己再多尝试就能很好地掌握。

因为更喜欢在实战中学习, 所以前期只是了解了大致思路没有特别深入的学习理论知识, 就直接开始码代码了。 在具体实现的过程中遇到问题, 再去看书或是网上寻找答案。

设计和实现

我选择写一个动态语言的解释器, 而不是静态语言的编译器。

之所以要写解释器, 不是因为我更偏好动态语言, 其实相比而言我更喜欢静态语言。 真正的原因是, 我觉得这只是***次尝试, 很多东西都不会, 肯定会写得很烂的,不如先就写动态语言, 等真正学得比较好了, 再回过头来写一门自己喜欢的语言。

正式开始写代码前, 我还要给这门语言取个名字,虽然只是个练手的项目, 不过还是得有个名字吧。 取名字还正不是一件容易得事, 就像给函数或者类取一个恰如其分的名字一样。

听说恰当的函数名或类名还能反应整个项目的设计是否合理, 逻辑清晰, 语言的名字似乎并没有这样的意义。

我脑袋里闪过的***个名字是Cactus(仙人球, 仙人掌)。 我觉得很喜欢这个名字, 就把Cactus暂时留个我要写的静态语言了(希望我真的会写, 没有白留)。 仙人球是植物(正好是静态的), 同样带刺的动物是刺猬(Hedgehog)。 动物是动态的, 正好符合我要写的动态语言, 于是就叫Hedgehog了。

前面提到了lex和yacc, 我在自己写的编程语言里面也使用了这两个工具做词法分析和语法分析。 既然是自己要写一门语言为什么还要用别的工具呢? 当然不能以”不重复造轮子”作为借口, 我就是为了造轮子才想要自己写编程语言的, 真正的目的是为了简单。

前面提到我把这当作一个练手的项目, 为了熟悉整个过程, 我把简单作为了整个过程的一个原则,很多地方我可以想到更优但更加复杂的实现方式, 但是大多数仍然采用了最简单最能保持整个项目逻辑清晰的实现方式。 我更多的目的是为了了解整体过程, 整体结构, 所以局部就尽量保持简单了吧(当然比较懒也是重要原因)。

当然后续词法分析和语法分析肯定会自己实现一下, 毕竟这算是编译器或者解释器的前端, 也很重要的。

解释器是用C语言写的。 之前从来没有用C语言写过这么大的项目(虽然到目前一共也就2千多行的代码), 这次也让我学会了很多C语言的高级用法。 比如 :

  1. void (*func)(void) 

是一个返回值为空, 参数为空的函数指针;

  1. void (*signal(int signo,void(*func)(int)))(int); 

是一个返回值为函数指针, 参数为(int signo,void(*func)(int)), (一个int, 一个函数指针)的函数, 其中函数名为signal。

之所以用函数指针, 是为了用C语言写面向对象, 最开始我完全是使用面向过程, 只是简单的通过不同文件实现简单的封装。

后来越写越大, 就出现各种问题, 比如头文件交叉引用引起编译器报错。 还有很多地方用面向对象可以更好地实现, 比如要处理表达式的创建和求值, 如果能有一个表达式的接口, 就能利用多态的好处, 不需要再写一个巨大的switch, case语句, 使用枚举来判断不同的表达式, 调用不同的函数。

我听说限制程序员的不是编程语言而是编程思维, C语言当然也可以写面向对象, 数据可以封装在结构体中, 再给结构体加上函数指针就实现了类的方法。

多态也可以通过自己实现虚函数表, 在对象初始化时把函数指针指向不同的函数就实现了。 大多数的面向对象的特性都有相应的方法实现, 只不过是语法上不如原生支持面向对象的语言简单罢了。

还有一些问题是关于这门语言本身的设计问题:

(1) for, if这类的语句中变量的作用域问题

一开始我设计的是Java, C++一样的, for, if的代码块中声明的变量, 它作用域只存在于整个代码块中。 后来想到了这是一门弱类型的动态语言, 独立的运行环境也没什么特别的用处, 于是就改成了和Python一样: 这种代码块都没有独立的运行环境。

(2) 把函数看成什么的问题。

比如Java这种纯粹面向对象的语言, 函数只能是对象的方法。 我这里是把函数作为一种基础数据类型, 像字符串一样, 可以直接用于传参, 赋值。

毕竟这是自己的编程语言,可以把它设计成自己喜欢的样子, 所以大多数的设计都是根据自己的想法, 自己觉得怎么合理就怎么来(当然不是天马行空地胡乱设计, 而是根据自己地实际经验选择合理的设计吧)。

当然最开始写一门编程语言的时候,有很多地方不知道怎么设计才合理, 这个时候我就参考自己学过的编程语言, 想想它为什么要采用这种设计, 出于怎样的考虑。

这样的思考, 让我对之前学过的编程语言有有了更加深刻的认识, 可以说是受益匪浅吧。 我渐渐地也认识到编程语言的设计很多时候都是设计者编程思维的体现。

简单地介绍一下Hedgehog

说了这么多, 是时候简单地介绍一下我写的这门编程语言了。目前还很简陋, 后面再慢慢地完善它吧。

hedgehog 的多数设计和 python 比较相似, 无需声明变量类型, if,for等语句没有块级作用域。

语法上又有点像 go 语言: if, for后面不需要(), 但是后面的代码块都必须加{};

没有while, 不过有for condition {}来替代。 不过行尾必须加;这点和 go 不同。

大多数设计都是为了简化实现方式, 比如必须加{}, ;是为了简化语法的解析。

数据类型

  1. a = 10;  //int 
  2. b = 3.14;//float 
  3. c = true;//boolean 
  4. d = null;//null 
  5. s = "Hello, World!";//string 

控制语句

  1. a = 10; 
  2. if a > 10 { // `()` is not necessary. 
  3.     b = a+20; 
  4. } elsif a==10 { 
  5.     b = a+10; 
  6. else { 
  7.     b = a-10; 
  8. print(b); 

循环

  1. for i=0; i<10; i=i+1 { 
  2.     print(i); 
  3.     if i>=4 {break;} 
  4. i = 0; 
  5. for i<10 { 
  6.     if i<5 {continue;} 
  7.     print(i); 

函数

function也被看作一种值(基本数据类型), 不过目前还没有对它实现垃圾回收, 所以直接以函数赋值或者其他操作会出现内存错误。

  1. // 模仿python首页的函数 
  2. func fbi(n) { 
  3.     a, b = 0, 1; 
  4.     for a<n { 
  5.         print(a); 
  6.         a, b = b, a+b;//支持这种赋值方式 
  7.     } 
  8. fbi(100); 
  1. func factorial(n) { 
  2.     if n==0 {return 1;} 
  3.     return n*factorial(n-1); 
  4. print(factorial(5)); 

目前只实现了一个原生函数print。 print接收一个基本数据类型作为参数, 输出并换行, 或者无参数, 直接换行。

运算符

大多数与c保持一致, 除了&, |。 因为没有提供位运算的功能, 所以直接用这两个符号表示逻辑与和逻辑或。

  1. b = 2; 
  2. a = 10; 
  3. if a>20 & b<10 { 
  4.     print("`b` is less than 10 and `a` is greater than 20"); 
  5. if a>20 | b<10 { 
  6.     print("`b` is less than 10 or `a` is greater than 20"); 

“What I cannot create, I do not understand。” 我喜欢这种从自己制作过程中学习的方式。 这种方式给了我一种踏实感, 让我觉得自己是真地明白了整个过程,而不是仅仅记住了什么公式, 学会了调用新的API。

【本文为51CTO专栏作者“刘欣”的原创稿件,转载请通过作者微信公众号coderising获取授权】

戳这里,看该作者更多好文

【编辑推荐】

  1. 热点 | 我用Python告诉老大爷“啥是佩奇”
  2. 从C++转Python:要改变你的思维方式
  3. 我是Java,这是我的2018年总结
  4. Java能抵挡住JavaScript的进攻吗?
  5. 小心踩雷,一次Java内存泄漏排查实战
【责任编辑:武晓燕 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢