九章编程: 文言文编程的 Cirru 实现的一种试验

本文是对于 wenyan-lang 方向在的一些尝试, 利用 Cirru 的工具链, 做简化的方案.
代码实现看九章编程, 以及对应的 Demo 页面.

基于九章编程的方案, 最终实现一个 Fibonacci 函数, 代码是这样的,

术曰 菲氏数 (甲)
  若 (少于 甲 三) 一
    并 (菲氏数 (减 甲 一)) (菲氏数 (减 甲 二))

得 (菲氏数 一)

对比一下文言编程的例子, 会显得后者啰嗦很多.

吾有一術。名之曰「斐波那契」。欲行是術。必先得一數。曰「甲」。乃行是術曰。
    若「甲」等於零者乃得零也
    若「甲」等於一者乃得一也
    減「甲」以一。減「甲」以二。名之曰「乙」。曰「丙」。
    施「斐波那契」於「乙」。名之曰「丁」。
    施「斐波那契」於「丙」。名之曰「戊」。
    加「丁」以「戊」。名之曰「己」。
    乃得「己」。
是謂「斐波那契」之術也。

施「斐波那契」於十二。書之。

当然了, 九章编程方案只是取巧地把 Lisp 的写法翻译成中文而已, 少了很多.
首先这东西挺好玩的. 再就是自己仔细看下来也有不少自己的想法.

古文用法的一些想法

翻了代码里的问题, 文言编程几个作者古文词汇和句式都比我丰富挺多的.
不过因为现在的人都不是常用古文, 其实也挺不正宗的.
大致能感受到例子里很多用法, 是混杂了不同朝代的措辞, 所以有点怪.
而且有些无法解决的问题, 西方传入的数学和计算机术语, 免不了要用现代的词汇.

当然, 按照我的偏好, 如果说有古文编程的话, 我首先想到用古代典籍作为模板.
比如说九章算术, 至少汉代的著作了, 这应该能充分代表古人对于数学的表达习惯.

〔三七〕今有環田,中周九十二步,外周一百二十二步,徑五步。問為田幾何?
荅曰:二畝五十五步。

〔三八〕又有環田,中周六十二步、四分步之三,外周一百一十三步、二分步之一,徑十二步、三分步之二。問為田幾何?
荅曰:四畝一百五十六步、四分步之一。
術曰:并中外周而半之,以徑乘之為積步。
密率術曰:置中外周步數,分母、子各居其下。母互乘子,通全步,內分子。以中周減外周,餘半之,以益中周。徑亦通分內子,以乘周為實。分母相乘為法,除之為積步,餘積步之分。以畝法除之,即畝數也。

以及元代的四元玉鉴当中也有类似的表达习惯.(没有句读太难读了..)
我觉得如果这些数学家当年发明编程语言的话, 怕是跟这差不了多少.
不过也还好现在的编程语言有各种标点符号, 不然真是有的受了.

另外比较明显的一个麻烦是, 代码当中必然会有较多的抽象, 或者说定义函数,
即便大家用的不是 Lisp 这样的前缀表达式语言, 也少不了会遇到这样的代码,

(f1 p1 p2)
(f2 q1 q2 q3 q4 (f3 a5 q6) (f4) q7 q8)

算了我还是换个你们好接受一些的写法:

f1(p1, p2)
f2(q1, q2, q3, q4, f3(a5, q6), f4(), q7, q8)

在文言编程当中, 可以看到 wenyan 用了 名之曰「丁」 来定义操作,
而实际上对应这种枯燥的抽象, 基本上很难也古文自然得表达出来.
或者说代码, 作为给机器执行的语言, 本身就有着特殊性.

当然, wenyan 能定义出这么一整套来, 还是挺厉害的.

在九章编程当中, 我出于省事的考虑, 直接基于已有的 Lisp 风格直接做了.
也就是说, 九章编程基本上就是基于前缀表达式实现的. 不像自然语言.
但是具体的术语, 我基于九章算术的文本做了简单的统计, 选取了一些词汇,
总得来说只是借了一层九章算术花样, 比如"对象", 九章算术里压根就没这东西.

中文数字和变量名的一些处理

我看 wenyan 当中用的中文数字表示, 在源码里有自己去解析和搜集.
翻了一下代码, 大概是自己进行了解析吧, 先转成阿拉伯数字表示, 就很快了.
九章编程里面直接找了个模块 nzh 进行转化的, 做 Demo 也够用.

另一个是变量名的问题, 九章编程直接用中文字符串做的.
因为九章编程实际上是 interpreter, 不是转义 js 的, 没这个限制.
wenyan 的实现当中我看到有转成拼音的操作, 不确定具体情况.
按说中文, 都是 jia3, 虽然有字典, 但很容易会重名的.

JavaScript 方言的实现方案

wenyan 大致上提供了 js, py, ruby 的方案, 大致看了一下 js 的部分.
首先 tokenize, 再 parser 解析代码, 然后用 compiler 拼接 js.
拼接 js 的部分是直接用的字符串拼接, 相对来说不那么完善, 但是够用.
另外手动拼接得到的代码, 一般格式都是乱的, 需要用 Prettier 或者 Babel 重新格式化.

另外比较省事而且可靠点的方案是用 babel/generator 去实现.
就是说用 Babel 来处理 JavaScript 代码生成的具体实现,
这样几方的工作只要做到能生成 AST 就好了, 这就安全很多.
比较熟悉是因为我的 CirruScript 用的就是这套方案, Babel 工具链真挺丰富的.

九章编程用的方案是 Interpreter, 解释执行, 没有生成 js 代码.
这也就意味着执行计算都是在 JavaScript 运行环境内部的,
单纯 JavaScript 执行, 可以有 V8 优化, 最终甚至可能以汇编的形式运行,
那样来说性能就好很多了. 解释执行的问题就是性能会很差.
不过另一方面, 解释执行不需要满足 js 语法, 也就没七七八八的限制了. 直接跑.

Cirru 提供的方案

虽然对于编译器来说, 生成代码的优化是最难的部分, 但玩具项目的话...
要写个 Parser 把整个代码结构解析出来也是相当要命的工作量.
wenyan 光是 parser.js 就八百多行了, 还不算各种工具函数和关键字定义的,
没看明白 typechecker.js 具体逻辑, 校验结构么, 也快七百行了.
反而 compiler 生成 js 部分三百多行就搞定了...

我...毕竟是写着玩的, 如果 Parser 也要这么风风火火折腾一遍, 枯燥啊.
不过我有 Cirru 这边的工具链, 加上语法, 直接用 Lisp 风格套上去了.
Cirru 大致是是一套把缩进语法(或者数据)生成一个树结构的方案,
比如这样一段代码, 直接用 Cirru 的模块进行解析,

得 (菲氏数 一)

就能直接得到一个树形的结构:

[
  [
    "得",
    [
      "菲氏数",
      "一"
    ]
  ]
]

前面函数定义的部分, 代码复杂一些, 有缩进, 也对应解析出来:

术曰 菲氏数 (甲)
  若 (少于 甲 三) 一
    并 (菲氏数 (减 甲 一)) (菲氏数 (减 甲 二))

得到着要一个树形的结构:

[
  [
    "术曰"
    "菲氏数"
    ["甲"]
    [
      "若"
      ["少于" "甲" "三"]
      "一"
      [
        "并"
        [
          "菲氏数"
          ["减" "甲" "一"]
        ]
        [
          "菲氏数"
          ["减" "甲" "二"]
        ]
      ]
    ]
  ]
]

有这样一个结构, 后面的部分就相对容易了, 如果不校验的话, 直接就能算, 比如:

["减" "甲" "一"]

经过简单的变换就能得到对应的 JavaScript 代码:

(甲 - 1)

或者更加复杂一些的结构,

[
  "并"
  [
    "菲氏数"
    ["减" "甲" "一"]
  ]
  [
    "菲氏数"
    ["减" "甲" "二"]
  ]
]

其实就是判断一下, 对中间的数组进行递归计算, 也很容易完成求值.
当然, 具体到函数定义方面, 以及一些动态长度(或者复杂节够)的语句, 会麻烦一些.
原理上可以参考 http://norvig.com/lispy.html 提供的例子.

这套方案用了 Cirru 的 Parser, 同时也就继承了 Cirru 语法的约束,
比如用 ( ) $ , " 以及空格进行语法结构控制的事情.
放在九章编程里面, 主要是在古文编程当中插入了大量的英文符号...
或者说这些如何其实除了空格换成中文笔画符号.. 可能效果也是类似的, 总之有些奇怪的东西.
不过总体上讲, 直接省去了大量工作量.

其他

wenyan 高亮做得比较充分, 渲染图挺漂亮的. 九章这边没有专门做.
不过倒也不是一点高亮都没有, 可以看到文档里直接用 Cirru 进行了基础的高亮.
看源码那边, 好像 wenyan 用的是 SVG 渲染的图, 效果确实不错.

因为是个玩具项目, 九章编程试验到能求 Fibonacci 然后, 有点玩不动了.
我知道后面的工作量挺多的, 比较我之前 Cirru 项目当中就在尝试.
有兴趣过来 watch 一下 CirruScriptinterpreter.nim 这边的工作.
虽然暂时没有经历深入开发, 但是断断续续会王中间追加功能的.

另外我在微博上也有提到, 中文的表达能力其实是非常强的,
可以想象, 同个含义, 在古文当中可以得到多个表述, 排列组合下来, 不比英文少...
以往看到的中蟒算是做的比较完整的一个范例吧.
完全有很多可能, 可以脑补一个少儿编程的场景, 写一段代码,

李雷的数 是 1
韩梅梅的数 是 2
(李雷的数 加上 韩梅梅的数) 是多少

这个代码用 Cirru 能解析出来一个简单的结构,

[
  [
    "李雷的数",
    "是",
    "1"
  ],
  [
    "韩梅梅的数",
    "是",
    "2"
  ],
  [
    [
      "李雷的数",
      "加上",
      "韩梅梅的数"
    ],
    "是多少"
  ]
]

做一下语法转换, 就得到一串很熟悉的前缀表达式了,

[
  [
    "是",
    "李雷的数",
    "1"
  ],
  [
    "是",
    "韩梅梅的数",
    "2"
  ],
  [
    "是多少",
    [
      "加上",
      "李雷的数",
      "韩梅梅的数"
    ]
  ]
]

然后求一下值, 拿去忽悠一零后有没有效果...

换成中文门槛也低一些吧, 应该有不少可以尝试的.
九章编程的 Demo 是可以执行的, 欢迎试玩 http://jiuzhang.cirru.org/

阅读 636

推荐阅读
题叶
用户专栏

ClojureScript 爱好者.

496 人关注
249 篇文章
专栏主页
目录