Skip to content

Latest commit

 

History

History
143 lines (102 loc) · 8.37 KB

File metadata and controls

143 lines (102 loc) · 8.37 KB

第 10 节

本节最后修改于 2023 / 04 / 02

基于设计模式的指令生成

我们讨论了很多指令设计模式相关。 与计算机领域相似的,一旦我们得到了一个较为普遍的原理,我们便可以写入程序,大规模地使用。

于是,一个野心勃勃的计划被提出来了:我们可以使用更简洁的语言来描述指令,最终使用某种解析器搭配指令设计模式对其中的细节进行补全。 这个计划更通俗的描述,就是“指令生成”。

这个计划中的“更简洁的语言”学名叫做领域限定语言(DSL),为了方便,我们可以管它叫“代指令”。 由于我的世界指令语法十分拉跨,没有编程语言应有的大部分特征,甚至其本身就是一种通用性较弱的 DSL,所以我们不可能利用指令写一个解析器来实现代指令,而需借助外部的力量。 考虑到我们最终需要搞一个解析器,这种“外部力量”与实现解析器的编程语言就有很大关系了。 从灵活性的角度来讲,使用 C/C++ 作为代指令是不错的选择,甚至可能还有接轨 LLVM(一种适用于各种编程语言的黑魔法“解析器”)翻译成其他语言的光明前景。 但是从跨平台性以及本人的代码水平方面来讲,我觉得 JavaScript 是一个更好的选择。

代指令

由于本人十分抵触复杂的编译原理,代指令可以是由各种 API 构成的 JS 的一种内部 DSL。

听起来比较抽象,意思就是说代指令实际上不能算一种新的语言,而只是一种有特殊代码风格的 JS。 举个例子,比如我们如果想判断现在有没有实体,如果有的话就说“玉米真的好帅”,否则就“玉米实在太帅了”。

// 如果代指令是一种基于 JS 的新的语言
if ('@e') {
  say('玉米真的好帅');
} else {
  say('玉米实在太帅了');
}
// 如果代指令是一种由 API 实现的 JS 的内部 DSL
If(select('@e'))
  .Then(() => {
    Command.say('玉米真的好帅');
  })
  .Else(() => {
    Command.say('玉米实在太帅了');
  });
/*
 *   这里 If、select、Then、Else 等都是预先声明好的函数,
 *   也就是所谓的 API。
 */

不难发现,上面的第二个示例中——也就是我们的做法——相较于第一个示例,更加的复杂、繁琐,也没那么易懂。 但是第二个示例可以直接作为 JS 运行,也就是说我们可以通过直接运行这个有特殊代码风格的 JS 来对其进行解析,而不是对其进行各种繁琐的各种静态分析。

于是我们就这样绕过了编译原理……实际上没有。 由于代指令的结构较为复杂,我们实际上还是要由代指令对 API 的调用生成抽象语法树(AST),再通过对 AST 的操作实现指令的生成。 相当于我们是只绕过了词法分析和语法分析。

解析器

知道了代指令的形式之后,我们可以着眼于另一重点——解析器。

上面已经提到,我们使用 JS 编写解析器。 实际上本人赶潮流,最近 TypeScript 很流行,我是用的 TS 写的解析器。 这些都不重要,重要的是解析器的基本架构。

  1. 运行代指令

    这一部分帮助我们跳过了词法分析和文法分析,充当了对代指令进行分析的角色,所以这部分也可以叫做“分析”。

    代指令中包含对 API 的调用,我们需要使用一些奇技淫巧完成这一步骤。 就拿本文中的MP模块做示例,每个MP模块实际上都可以看作是一个函数:有输入值,有返回值。 这意味着为了使结构更加清晰,我们也需要让代指令在声明函数后可以直接调用。 例如声明一个夸赞玉米MP模块,再调用它:

    // 本人猜想,仅供参考
    const func = Fn(() => {
      Command.say('玉米太帅');
    });
    
    func();

    一个很显而易见的事实是,我们一定要让 Fn 这个 API 返回一个真正的函数,要不然我们 func(); 就会在运行时报错。

    对于 API 的设计,这里就不展开讲了。

    实际开发中,发现最终得到指令的一个 AST 更方便接下来的工作。 所以 API 本质上是一堆对 AST 进行增删改查等工作的函数。

  2. “补全指令细节”

    在得到指令的 AST 后,树上的各种条件分支、条件运算、函数声明等节点让人心惊胆战,我们根本没法通过这么花里胡哨多彩多样的 AST 直接生成对应的指令。 于是我们不得不对 AST 进行转译——这个步骤的正式名称叫做转译。

    我们在其中插入更多“平平无奇的命令节点”来代替各种花里胡哨的其他节点,最终把整颗花里胡哨的树压成两层的数组来表示每个命令方块组中的每个指令。

    如何使用普通的命令节点代替这些结构性节点呢? 这便是本文中指令设计模式发挥作用的时候。 就像条件分支,本文中使用MP过程这一基于标签或计分板的封闭系统,而MP过程的特征是普遍的。 就如“套路”、“模板”一样,我们只需要根据对应的模式,把对应的东西套上去就可以了。

    除了本文所描述的各种设计模式,还可以提供些额外服务。 这里举个例子:

    有些条件分支的条件是一个命令是否成功运行,相对应的,条件也可以是一个函数是否运行成功。 根据我们的MP模块相关知识,这个条件实际上是一串命令方块是否全部运行成功。 一个简单的方法是将这串命令方块全弄成有条件命令方块,然后在这串命令方块最后插入一个给任意实体打标签的连锁命令方块。 检测到是否任意实体有这个标签,可以判断最后这个命令方块是否成功运行,也就是整串命令方块是否成功运行。 如此,我们便将一个条件分支节点转化为了一个打标签命令和一个检测标签的命令。

    但是,如果这串命令方块中已经有命令方块是有条件命令方块,我们又要怎么判断是否成功运行呢? 这种情况下,一个比较合理的解决方法是在有条件命令方块的前一个命令方块前面把命令方块串劈成两半,视作两个命令方块组,分别检测是否成功运行。 最终判断两个命令方块是否都成功运行,若是则整条命令方块串都成功运行。

    这虽然不属于本文所介绍的设计模式之一,但确实可以应用到程序中。

    转译便是通过上述这些“指令设计套路”来对 AST 进行处理。

  3. 输出指令

    当我们有了一个结构简单的转译过的 AST 后,实际上我们就得到了我们生成的指令。 我们唯一需要做的是把这个 AST 弄为人类理解的样式。

    虽然这个步骤不是最困难的,但这个步骤是最困难的。 让人理解命令方块是一个比较困难的东西,现在貌似也没有普遍的描述命令方块的标准。 图片等可视化的也是一种选择,但是太麻烦了。 实际上本人还有一个项目是命令方块图片生成器,如果能直接套用这个项目就很好,但是目前这个项目进度缓慢,估计是指望不了了。

    当然,还有一种方法是直接生成 OOC,但是目前基岩版还不能这么搞。

宣传

作为一个惊天地泣鬼神前无古人后无来者的看起来挺有用的玩意,却没有人关注,无疑是十分蛋疼的。 于是宣传便成为了比较重要的一个环节。

其实可以说我现在就是用一个章节对这个东西进行宣传——当然还是希望本章节确实对诸读者有一定启发性——所以大概意思就是这玩意我确实搞起来了。

项目是 GPL-3.0 协议开源的,大家可以访问代码仓库,一起来搞这个项目。

相对于手动使用设计模式进行重复性劳动,将设计模式套入程序中生成指令无疑是生产力的大进步。 在这个项目完成后,我希望可以借助这个项目代替我编写指令,我的注意力便可从“使用设计模式”移到“发现更多设计模式并应用到这个项目”。 我也希望诸读者可以试着采取我的这种方法。 如果大家都把使用设计模式的精力放到发现设计模式上,就像共产主义摆脱了低级劳动,那一定是《MC设计模式》的新世纪。