Java 的 Parser combinator 中最有名应该是 JParsec 了。随着 Java 8 的发布,我们也可以用 lambda 表达式来写规则了。刚开始的时候我以为它跟 C# 下面的 Sprache 会比较像,API 应该很容易。结果我错了。Sprache 的思想跟《Monadic Parser Combinators》这篇论文里的提到的设计方法如出一辙,是不区分 lexer 和 parser 的。但是 JParsec 区分。如果在 parser 对应的规则里面使用 lexer 的规则,会导致异常。可以参看 ParserState 类:
@Override CharSequence characters() {
throw new IllegalStateException(
"Cannot scan characters on tokens.");
}
如果你在尝试用 JParsec 写规则时遇到了不知道怎么在 parser 规则中引用 lexer 规则的问题,恭喜你,看完下面的代码你会应该知道怎么做了。
整体示意图:
比如解析一段 key/value 文本:
"update": "2014-10-28"
为了解析双引号的部分写一个 lexer 规则:
private static final
Parser<Tokens.Fragment> STRING_LITERAL =
Scanners.DOUBLE_QUOTE_STRING
.map(patchTag(Tag.STRING))
.label("string literal");
enum Tag {
STRING
}
private static
Map<String, Tokens.Fragment> patchTag(Object tag) {
return str -> new Tokens.Fragment(str, tag);
}
除 patchTag 函数和 Tag 枚举之外其它的都是 JParsec 的 API。patchTag 的作用就让 lexer 在运行的时候将 STRING_LITERAL 规则所解析出来的字符串打上 STRING 这个 tag。这样在后面的 parser 规则部分用这个 tag 将字符串规则取出来:
private final
Parser<String> stringLiteral =
Parsers.token(withTag(Tag.STRING))
// 除掉引号
.map(s -> s.substring(1, s.length() - 1))
.label("string literal");
private static TokenMap<String>
withTag(Object tag) {
return token -> {
Object value = token.value();
if (value instanceof Tokens.Fragment) {
Tokens.Fragment fragment =
(Tokens.Fragment) value;
if (tag.equals(fragment.tag())) {
return fragment.text();
}
}
return null;
};
}
这样就可以做一个规则来匹配 "update": "2014-10-28" 这样的文本了:
private Parser<StatAstKeyValue> keyValue =
Parsers.sequence(
stringLiteral,
OPERATORS.token(":"),
stringLiteral,
(k, ignored, v) ->
new StatAstKeyValue(k, v))
.label("keyValue");
private static final
Terminals OPERATORS =
Terminals.operators("{", "}", ",", ":");
文本中的空格部分要求在 lexer 中额外定义一个空白字符的规则:
private static final
Parser<Void> WHITESPACES = Scanners.WHITESPACES;
为了达到忽略空白字符的目的,我们可以用 Parser#skipMany() 这样的 combinator,也可以用 JParsec 的官方教程上的做法:
private Parser<List<Token>>
TERMINALS = TOKENIZER.lexer(
WHITESPACES.or(Parsers.always())
);
这里将 WHITESPACES.or(Parsers.always()) 作为分隔符来分隔 TOKENIZER 解析出来的 token。如果不加 Parsers.always(),会导致可以解析
"k" : "v"
但不能解析
"k": "v"
注意第 2 个例子中冒号的前面没有空格。