在Java中编写自定义语法解释器?

4
我将要开始编写一个演示程序,用于我即将要授课的课程。我希望让班上每个学生都可以下载此应用程序,并能够通过命令行交互式地创建对象实例(及其图形表示)。我决定使用Java来编写,不是因为它是我最熟悉的语言,而是因为它有易于使用的图形类,并且我可以相当确信该jar文件可以在他们的计算机上运行。
介绍完毕,现在问题来了:
如何为此程序实现一些自定义命令行语法?我想使用简单的任意语法,例如:
CREATE Monster Bob;    
Bob.jump();   
LS Bob //to list Bob's methods or something.   
LS CREATE //to list all the classes    

首先,我来谈谈当我想到这个问题时首先想到的是什么。我可以想象,我可以拥有一组树形链接的地图。我可以将每个关键字解析为下一个地图的关键字。因此,“CREATE Monster Bob” 可以像这样进行评估:
1) 搜索关键字地图以查找“CREATE”键。返回值,即类地图的引用。 2) 在类地图中搜索“Monster”键。返回值,即实现某个接口 Leaf 的工厂类,让我知道它是一个叶子值(我将使用 instanceof 进行检查)。 3) 可能 Leaf 接口将包含一个名为 execute() 的方法,该方法将执行它想要执行的任何操作。在这种情况下,它将创建一个 Monster 对象,并将此对象添加到名为 Bob 的 Objects 地图中。(这个 Leaf 业务听起来很丑陋,但它可以被整理干净。)
很酷。但这个语句对我来说有点难度: Bob.jump();
1) 搜索某些对象地图以查找“Bob”。返回实现具有类似于“evaluate(String s)”方法的接口的某个对象,并将字符串“jump()”传递给它。 2) Bob 在其内部方法映射中搜索“jump()”,然后...?在 C++ 中,我会将键作为指向成员函数 Monster.jump() 的指针,该函数将被执行。但我不认为在 java 中有这样的函数指针。我已经阅读过可以使用匿名类来实现这一点,尽管我还没有尝试。看起来它会起作用。
因此,这将起作用,但是否有更优雅的方法?我以前从未编写过任何类型的解释器。如果有人有一些技巧,我希望能够以好的方式做到这一点并在此过程中学习一些东西。如果我没有很好地结构化,特别是当 Bob 和每个其他对象开始解析自己的指令并使用匿名函数时,这似乎是一个潜在的容易出错的方法。此外,似乎每个类都需要除其正常代码之外的运行时准备接口。
此外,我对 Java 不是很了解,所以如果有一些问题可能会碰壁,那么我也想知道。
提前感谢您的帮助。

@pst:我认为在这里使用ANTLR有些过头了。 - Andrey Agibalov
3个回答

10
我建议使用Python,除非有一个非常好的理由不这样做。
这是因为:
  1. Python有一个非常好的IDLE/REPL。我无法说足够多关于使用良好的Read-Eval-Print-Loop:短的反馈循环对于学习/玩耍非常有益。冒险的学生甚至可以直接开始!
  2. 图形支持是跨平台的,并且通过TkInter得到良好支持。
  3. 我发现它比Java更适合初学者和/或非程序员。 (实际上,Python并不是我的最爱语言,但它非常适合初学者,并且再次拥有一个非常好的IDE/REPL。)
  4. 这对你来说会少得多的工作 ;-)
以下是演示的Python代码可能如何看起来:
Bob = BigMonster()
Bob.jump()
dir(Bob)
dir(Monters)

既然这只是普通的 Python 语法,所以没有解析 - 只需创建一些类,可能实现 __dir__ 协议,一切就准备好了。如果需要 Java 集成,也可以使用 Jython,尽管我从未尝试过在 IDLE 中使用它(或者不知道是否支持)。

愉快编码。

Sqeak 这样基于图像的 SmallTalk 比 Python 更具交互性,因为代码是持久运行环境的一部分。然而,找到一个好的图像需要一些时间 - Squeak 不是最好的实现,但它是免费的 - 并学习特定的 SmallTalk 环境。因此,虽然集成最终可能会有很大的回报,但需要更多的适应 :)


然而,要在Java中追求一个简单的解析器,需要以下内容:

  1. 将输入文本转换成标记流的词法分析器
  2. 以及一个递归下降解析器(这是一种非常简单的解析方法),它可以构建一个抽象语法树(AST),稍后可以遍历(即“运行”),或者
  3. 立即执行“操作”

一个简单的递归下降解析器是介绍上述概念的Java速成课程。这里有一些关于“中微子语法”的递归下降解析器的代码--查看注释以及递归下降解析器如何很好地匹配EBNF语法。

现在,只需要定义这种伪/小型语言的语义规则并实现它即可;-)


我将更深入探讨语义/Java方法(部分内容只是对原帖的简化/重新陈述):

CREATE Monster Bob

将创建一个新的MonsterObject。一些方法可能包括:

  1. 使用反射创建对象, 或者;
  2. 一个从字符串到FactoryObject的工厂类映射,或者;
  3. 一个简单的静态if-else分支。

结果将存储在“变量哈希”中,它将Name映射到MonsterObject。

Bob.jump()

将此解析为[object Bob] [method jump] [p1], [p2], ..., [pn],在"变量哈希"中查找对象,然后:
  1. 使用反射调用方法;或者
  2. 有一个Name -> MethodEvaluatorObject(例如具有eval(Object... params)方法)的映射(通过MonsterObject的方法检索),或者
  3. 调用形式为eval(String action, String[] ... parameters)的方法,并让它使用if-else分支来“执行操作”(请注意,在解析期间,如果有任何参数,它们已经被分离出来了)。
LS BobLS Monster在很大程度上依赖于前两个的实现方式。
在Java中虽然没有"函数指针",但可以通过使用具有给定接口的对象来模拟它们(也就是说,这些对象本身作为指针)。Functional JavaF/F2/.../F8 类以尝试使用泛型统一处理这个问题。然而,在Java中通常会创建单独的接口(或类),比如Runnable,它只有一个"action"方法,该方法被修改以接受适当的参数并返回适当的结果(例如MethodEvaluatorObjects或FactoryObjects)。
如果有关于其中一个主题(反射、递归下降、匿名类型、[模拟的]闭包等)的任何具体问题,请随时提出另一个SO问题,重点是具体问题。(并且,像往常一样,进行尽职调查会得到回报;-)

那是一个非常完美的答案,感谢你付出的所有努力。现在该点击链接并开始阅读了。 - user487100
哇,反射正是我所需要的。我之前完全不知道。 - user487100

2
如果您真的不打算创建一个新的编程语言,您可以将命令拆分为几个部分(使用空格作为分隔符),然后查找第一个部分: CREATE Monster Bob; => create, monster, bob:
String operation = parts[0];
if(operation.equals(`create`)) {
  String type = parts[1];
  String name = parts[2];
  // your logic here
} else if(operation.equals(`...`)) {
  ...
}

我其实很喜欢这个。我们可以称之为MUD方法吗?哈哈。我认为这就是MUD通常实现命令的方式,对吧? - Anonymous

1
你有没有考虑使用像ANTLR这样的解析器生成器?它可以为许多种语言生成解析器,并以包括Java在内的多种语言输出解析器。这可以大大加快您的任务速度,而且该软件是免费的(尽管书籍是出售的,但嘿,您的时间也是值钱的,对吧?)。

http://en.wikipedia.org/wiki/ANTLR

另一方面,像PST所说的这样简单的语言,您可能可以自己编写解析器,但我不会过于复杂化。只需编写一个函数来将文件分割成字符串标记(词法分析器),以及另一个函数每次请求一个标记并确定要执行的操作。如果您的语言很简单,这可能已经足够了。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接