`
cloud19841207
  • 浏览: 727 次
社区版块
存档分类
最新评论

请您先登录,才能继续操作

利用 Java 实现组合式解析器

 
阅读更多

引用地址:http://www.ibm.com/developerworks/cn/java/j-lo-compose/

 

Ward Cunningham 曾经说过,干净的代码清晰地表达了代码编写者所想要表达的东西,而优美的代码则更进一步,优美的代码看起来就像是专门为了要解决的问题而存在的。在本文中,我们将展示一个组合式解析器的设计、实现过程,最终的代码是优美的,极具扩展性,就像是为了解析特定的语法而存在的。我们还会选取 H.248 协议中的一个例子,用上述的组合式解析器实现其语法解析器。读者在这个过程中不仅能体会到代码的美感,还可以学习到函数式编程以及构建 DSL 的一些知识。

 

DSL 设计基础

我们在用编程语言(比如:Java 语言)实现某项功能时,其实就是在指挥计算机去完成这项功能。但是,语言能够带给我们的并不仅仅局限于此。更重要的是,语言提供了一种用于组织我们的思维,表达计算过程的框架。这个框架的核心就在于,如何能够把简单的概念组合成更为复杂的概念,同时又保持其对于组合方法的封闭,也就是说,组合起来的复杂概念对于组合手段来说和简单的概念别无二致。引用“Structure and Interpretation of Computer Programs”(参见 参考资源)一书中的话来讲,任何一个强大的语言都是通过如下三种机制来达成这个目标的:

  • 原子:语言中最简单、最基本的实体;
  • 组合手段:把原子组合起来构成更复杂实体的方法;
  • 抽象手段:命名复杂实体的方法,命名后的复杂实体可以和原子一样通过组合手段组合成为更复杂的实体。

像 Java 这种通用的编程语言,由于其所关注的是解决问题的一般方法。因此,其所提供的这三种机制也都是通用层面的。在解决特定问题时,这种通用的手段和特定问题领域中的概念、规则之间是存在着语义鸿沟的,所以某些问题领域中非常清晰、明确的概念和规则,在实现时就可能会变得不那么清晰。作为程序员来说,用干净的代码实现出功能仅仅是初级的要求,更重要的是要提升通用语言的层次,构建出针对特定问题领域的语言(DSL),这个过程中很关键的一点就是寻找并定义出面向问题领域的 原子概念、组合的方法以及抽象的手段。这个 DSL 不一定非得像通用语言那样是完备的,其目标在于清晰、直观地表达出问题领域中的概念和规则,其结果就是把通用的编程语言变成解决特定问题的专用语言。

我们曾经在“基于 Java 的界面布局 DSL 的设计与实现”(参见 参考资源)一文中,构建了一种用于界面布局的 DSL,其中呈现了上述思想。在本文中,我们将以解析器的构造为例,来讲述如何构建一种用于字符串解析的 DSL,这种 DSL 具有强大的扩展能力,读者可以根据自己的需要定义出自己的组合手段。此外,从本文中读者还可以领略到 函数编程的优雅之处。

 

解析器原子

什么是解析器?最简单的解析器是什么?大家通常都会认为解析器就是判断输入的字符串是否满足给定的语法规则,在需要时还可以提取出相应的语法单位实例。从概念上来讲,这种理解没什么问题。不过要想定义出用于解析的 DSL,那么就需要更为精确的定义,也就是说我们要定义出解析器这个概念的确切类型。在 Java 中,我们用 interface 来定义解析器类型,如下:

interface Parser 
{ 
    public Result parse(String target); 
}

其中,target 为要解析的字符串,Result 是解析的结果,只要满足这个接口语义的对象,我们就称其为一个解析器实例。Result 的定义如下:

class Result 
{ 
    private String recognized; 
    private String remaining; 
    private boolean succeeded; 
	
    private Result(String recognized, String remaining, 
        boolean succeeded) { 
        this.recognized = recognized; 
        this.remaining = remaining; 
        this.succeeded = succeeded; 
    } 

    public boolean is_succeeded() { 
        return succeeded; 
    } 

    public String get_recognized() { 
        return recognized; 
    } 

    public String get_remaining() { 
        return remaining; 
    } 

    public static Result succeed(String recognized, 
        String remaining) { 
        return new Result(recognized, remaining, true); 
    } 

    public static Result fail() { 
        return new Result("", "", false); 
    } 	
}

其中,recognized 字段表示这个解析器所认识的部分,remaining 表示经过这个解析器解析后所剩余的部分,succeeded 表示解析是否成功,Result 是一个值对象。有了解析器的精确定义,接下来我们就可以定义出最简单的解析器。显然,最简单的解析器就是什么也不解析的解析器,把目标字符串原样返回,我们称其为 Zero,定义如下:

class Zero implements Parser 
{ 
    public Result parse(String target) { 
        return Result.succeed("", target); 
    } 
}

Zero 解析器一定会解析成功,不做任何语法单位识别并直接返回目标字符串。下面我们来定义另外一个很简单的解析器 Item,只要目标字符串不为空,Item 就会把目标字符串的第一个字符作为其识别结果,并返回成功,如果目标字符串为空,就返回失败,Item 的定义如下:

class Item implements Parser 
{ 
    public Result parse(String target) { 
        if(target.length() > 0) { 
            return Result.succeed(target.substring(0,1), 
                target.substring(1)); 
        } 
        return Result.fail(); 
    } 
}

Zero 和 Item 是我们解析器 DSL 中仅有的两个原子,在下一小节中,我们来定义解析器的组合方法。

 

解析器组合子

我们在上一小节中定义了 Item 解析器,它无条件的解析出目标字符串中的第一个字符,如果我们希望能够变成有条件的解析,就可以定义出一个 SAT 组合子,其接收一个条件谓词(predicate)和一个解析器,并生成一个复合解析器,该复合解析器能否解析成功取决于原始解析器的解析结果是否满足给定的条件谓词,条件谓词和 SAT 的定义如下:

interface Predicate 
{ 
    public boolean satisfy(String value); 
} 

class SAT implements Parser 
{ 
    private Predicate pre; 
    private Parser    parser; 
    
    public SAT(Predicate predicate, Parser parser) { 
        this.pre = predicate; 
        this.parser = parser; 
    } 

    public Result parse(String target) { 
        Result r = parser.parse(target); 
        if(r.is_succeeded() && pre.satisfy(r.get_recognized())) { 
            return r; 
        } 
        return Result.fail(); 
    } 
}

如果,我们想定义一个解析单个数字的解析器,那么就可以定义一个 IsDigit 条件谓词,并通过 SAT 把该 IsDigit 和 Item 组合起来,代码如下:

class IsDigit implements Predicate 
{ 
    public boolean satisfy(String value) { 
        char c = value.charAt(0); 
        return c>='0' && c<='9'; 
    } 
}

解析单位数字的解析器 digit 定义如下:

Parser digit = new SAT(new IsDigit(), new Item());

我们可以采用同样的方法组合出单个字母、单个大写字母、单个小写字母等解析器来。

接下来,我们定义一个 OR 组合子,其接收两个解析器,并分别用这两个解析器去解析一个目标串,只要有一个解析成功,就认为解析成功,如果两个都失败,则认为失败,代码定义如下:

class OR implements Parser 
{ 
    private Parser p1; 
    private Parser p2; 
	
    public OR(Parser p1, Parser p2) { 
        this.p1 = p1; 
        this.p2 = p2; 
    } 

    public Result parse(String target) { 
        Result r = p1.parse(target); 
        return r.is_succeeded() ? r : p2.parse(target); 
    } 
}

我们可以定义出一个新的解析器 digit_or_alpha,如果目标字符是数字或者字母那么该解析器就解析成功,否则就失败。代码如下:

判断是否是字母的条件谓词:

class IsAlpha implements Predicate 
{ 
    public boolean satisfy(String value) { 
        char c = value.charAt(0); 
        return (c>='a' && c<='z') || (c>='A' && c<='Z'); 
    } 
}

用于解析单个字母的解析器:

Parser alpha = new SAT(new IsAlpha(), new Item());

digit_or_alpha 解析器定义:

Parser digit_or_alpha = new OR(digit, alpha);

下面我们来定义一个 顺序组合子 SEQ,该组合子接收两个解析器,先把第一个解析器应用到目标字符串,如果成功,就把第二个解析器应用到第一个解析器识别后剩余的字符串上,如果这两个解析器都解析成功,那么由 SEQ 组合起来的这个复合解析器就解析成功,只要有一个失败,复合解析器就解析失败。当解析成功时,其识别结果由这两个解析器的识别结果连接而成。

为了能够连接两个 Result 中已经识别出来的解析结果,我们在 Result 类中新增一个静态方法:concat,其定义如下:

public static Result concat(Result r1, Result r2) { 
    return new Result( 
        r1.get_recognized().concat(r2.get_recognized()), 
        r2.get_remaining(), true); 
}

顺序组合子 SEQ 的定义如下:

class SEQ implements Parser 
{ 
    private Parser p1; 
    private Parser p2; 
	
    public SEQ(Parser p1, Parser p2) { 
        this.p1 = p1; 
        this.p2 = p2; 
    } 
    public Result parse(String target) { 
        Result r1 = p1.parse(target); 
        if(r1.is_succeeded()) { 
            Result r2 = p2.parse(r1.get_remaining()); 
            if(r2.is_succeeded()) { 
                return Result.concat(r1,r2); 
		   } 
	     } 
	     return Result.fail(); 
	 } 
 }

现在,如果我们想定义一个解析器用以识别第一个是字母,接下来是一个数字的情况,就可以这样定义:

Parser alpha_before_digit = new SEQ(alpha, digit);

接下来我们定义本文中的最后一个组合子:OneOrMany。该组合子接收一个解析器和一个正整数值,其生成的复合解析器会用原始解析器连续地对目标串进行解析,每一次解析时的输入为上一次解析后剩余的字符串,解析的最大次数由输入的正整数值决定。如果第一次解析就失败,那么该复合解析器就解析失败,否则的话,会一直解析到最大次数或者遇到解析失败为止,并把所有成功的解析的识别结果连接起来作为复合解析器的识别结果,OneOrMany 组合子的定义如下:

class OneOrMany implements Parser 
{ 
    private int max; 
    private Parser parser; 
	
    public OneOrMany(int max, Parser parser) { 
        this.max = max; 
        this.parser = parser; 
    } 
    
	public Result parse(String target) { 
        Result r = parser.parse(target); 
        return r.is_succeeded() ? parse2(r,1) : Result.fail(); 
	 } 
    
	private Result parse2(Result pre, int count) { 
        if(count >= max) return pre; 
        Result r = parser.parse(pre.get_remaining()); 
        return r.is_succeeded() ? 
            parse2(Result.concat(pre,r),count+1) : pre; 
    } 
 }

使用该组合子,我们可以容易地定义出用于识别由最少一个,最多 10 个字母组成的串的解析器,如下:

Parser one_to_ten_alpha = new OneOrMany(10,alpha);

本文的组合子就定义到此,不过读者可以根据自己的需要,用同样的方法容易地定义出符合自己要求其他组合子来。

 

抽象的手段

如果在 DSL 的构造中,仅仅提供了一些原子和组合手段,并且组合的结果无法再次参与组合,那么这个 DSL 的扩展能力和适用性就会大大折扣。相反,如果我们还能提供出抽象的手段对组合结果进行命名,命名后的复合实体可以像原子一样参与组合,那么 DSL 的扩展能力就会非常的强大,适用性也会大大增加。因此,抽象的手段在 DSL 的构造过程中是至关重要的。

敏锐的读者可能已经发现,对于我们的解析 DSL 来说,其实在前面的小节中已经使用了抽象的手段。比如,我们在 alpha,digit,digit_or_alpha 以及 alpha_before_digit 等复合解析器的定义中已经使用了抽象的手段来对其进行命名,然后可以直接使用这个抽象的名字再次参与组合。由于我们的解析器是基于 Java 语言中的 interface 机制定义的,因此,Java 语言中已有的针对 interface 的抽象支持机制完全适用于我们的解析 DSL。因此,我们就无需定义自己的特定抽象手段,直接使用 Java 语言中的即可。

相信读者已经从上一小节中的例子中看到组合、抽象手段的强大威力。在下一小节中,我们将给出一个更为具体的例子:H.248 协议中 NAME 语法解析器的构造。

 

一个 H.248 实例

在本小节中,我们将基于前面定义的解析器原子和组合子,实现用于识别 H.248 协议中 NAME 语法的解析器的构造。

H.248 是一个通信协议,媒体网关控制器使用该协议来对媒体网关进行控制。H.248 协议是一个基于 ABNF(扩展 BNF)文法描述的基于文本的协议,协议中定义了 H.248 消息的组成部分和具体内容。关于 H.248 协议的具体细节,我们不在本文中讨论,有兴趣的读者可以从 参考资源 中获取更多内容。我们仅仅关注其中的 NAME 语法定义,如下:

NAME = ALPHA *63(ALPHA / DIGIT / "_" )
ALPHA = %x41-5A / %x61-7A   ; A-Z, a-z
DIGIT = %x30-39             ; digits 0 through 9

我们首先来解释一下其中的一些规则,*63 其实是 n*m 修饰规则的一个实例,表示最少 n 个最多 m 个,当 n 等于 0 时,可以简略写成 *m。因此,*63 表示最少 0 个,最多 63 个。/ 表示或规则,表示两边的实体可选。()表示其中的实体必须得有一个。- 表示范围。因此,DIGIT 表示单个数字,ALPHA 表示单个字母(大写或者小写),(ALPHA/ DIGIT/ “_” )表示要么是个字母,要么是个数字,要么是个下划线。*63(ALPHA/ DIGIT/ “_” )表示,最少 0 个,最多 63 个字母或者数字或者下划线。两个实体顺序写在一起,表示一种顺序关系,ALPHA *63(ALPHA/ DIGIT/ “_” ) 表示,以字母开始,后面最少 0 个,最多 63 个 字母或者数字或者下划线。更多的规则可以参见 参考资源

根据前面的内容可以很容易地直接表达出用于解析这个语法规则的解析器来。如下:

class H248Parsec 
{ 
    public static Parser alpha() { 
        return new SAT(new IsAlpha(), new Item()); 
    } 

    public static Parser digit() { 
        return new SAT(new IsDigit(), new Item()); 
    } 

    public static Parser underline() { 
        return new SAT(new IsUnderline(), new Item()); 
    } 

    public static Parser digit_or_alpha_or_underline() { 
        return new OR(alpha(), new OR(digit(), underline())); 
    } 
    
	public static Parser zero_or_many(int max, Parser parser){ 
        return new OR(new OneOrMany(max,parser), new Zero()); 
    } 
    
	public static Parser name() { 
        return new SEQ(alpha(), 
            zero_or_many(64, 
            digit_or_alpha_or_underline())); 
    } 
}

可以看出,我们的代码和协议中的语法描述基本上完全一样,我们通过定义自己的面向解析的 DSL,把 Java 这种通用语言变成了用于 ABNF 语法解析的专门语言,符合 Ward Cunningham 关于美的代码的定义。最后,我们用该解析器来做一些关于 NAME 语法识别的实验,如下表所示:

输入字符串成功标志识别结果剩余字符串
"" false "" ""
"_U" false "" ""
"2U" false "" ""
"U" true "U" ""
"U{" true "U" "{"
"U2{" True "U2" "{"
"U_{" true "U_" "{"
"U123_{" True "U123_" "{"
"USER001" True "USER001" ""
"USER001{" True "USER001" "{"
"a0123456789
0123456789
0123456789
0123456789
0123456789
0123456789
0123456789"
True "a0123456789
0123456789
0123456789
0123456789
0123456789
0123456789
0123"
"456789"
分享到:
评论

相关推荐

    利用 Java 实现组合式解析器,基于 Java 的界面布局 DSL 的设计与实现(转载)

    标题中的“利用 Java 实现组合式解析器”指的是在编程中使用 Java 语言来构建一个解析器,这种解析器通常采用组合式解析器构造技术。组合式解析器是一种基于规则和模式匹配的解析方法,它将复杂的语法结构分解成小的...

    使用Java实现语言解释器.doc

    标题中的“使用Java实现语言解释器”指的是使用Java编程语言构建一个可以理解并执行特定编程语言...通过这种方式,不仅可以学习到语言解析和执行的核心概念,还可以了解到如何利用Java的库和语法来构建复杂的程序结构。

    Java下拉式菜单实现

    在Java图形用户界面开发中,下拉式菜单(即组合框或选择框)是非常常见的一种控件,它允许用户从一个列表中选择一个值。本文将详细介绍如何使用Java Swing库中的`JComboBox`类来创建和定制下拉式菜单,并通过具体的...

    JAVA3D交互式三维图形编程pdf版

    总的来说,"JAVA3D交互式三维图形编程"的PDF教程可能会涵盖以上所有内容,并可能包含实例代码和详细解释,帮助学习者理解如何利用Java3D创建引人入胜的3D应用。无论你是初学者还是有经验的开发者,这本书都将是掌握...

    直线型语言解释器 java实现

    这一步通常通过解析器完成,例如使用递归下降解析或者LL(1)、LR(1)等解析技术。对于直线型语言,由于其简单性,可能可以直接通过递归函数实现。 3. **语义分析**:在构建AST后,解释器需要验证语句的语义是否正确,...

    基于Java的源码-表达式语法解析库 parboiled.zip

    解析器组合子是一种函数式编程的概念,它允许我们通过组合小的、简单的解析规则来创建复杂的解析器。在Parboiled中,这些规则可以通过Java方法定义,每个方法代表一个解析操作。这种设计使得解析器的构造和维护变得...

    用java语言实现的Web浏览器

    这个项目的核心是利用Java的网络和GUI(图形用户界面)库来创建一个交互式的应用程序。以下是这个项目中涉及的一些关键知识点: 1. **Java基础**:首先,要理解Java的基础语法、类库和面向对象编程概念。包括类、...

    基于Cesium时空数据可视化后台Java SSM框架.zip

    本文将详细介绍如何结合Cesium和Java SSM框架实现时空数据的后台处理与前端展示。 1. **SSM框架基础** Spring作为核心容器,负责管理对象(如Bean)的生命周期和依赖注入。SpringMVC作为Spring的Web MVC框架,...

    java框架课程代码全解析

    Java框架课程代码全解析主要涵盖了Java开发中常用且重要的框架技术,这些框架是现代Java企业级应用开发的基础。本课程的目的是帮助学习者深入理解并掌握这些框架的使用,提升开发效率,为构建稳定、高效的后端服务...

    java编写的flash播放器带类库

    为了在Java中播放SWF文件,开发者通常需要实现一个解析器来读取并解释这些二进制数据,将其转化为可渲染的图形和交互事件。这个类库可能包括了SWF文件结构的解析模块,以及用于渲染图形、处理动作脚本和事件的组件。...

    java框架SSM项目案例

    SSM框架是Java开发Web应用时常用的三大组件的组合,分别是Spring、SpringMVC和MyBatis。这个项目案例提供了一个完整的SSM整合实例,帮助开发者深入理解和掌握这些技术的协同工作方式。 **Spring框架**是Java企业级...

    动态树的管理程序(基于jQuery Treeview实现)-java源码

    前端收到响应后,利用jQuery解析数据并更新Treeview的视图。 最后,压缩包中的"java"文件夹很可能包含了项目的后端源代码,包括Struts2的Action类、Spring的Service和DAO层以及可能的自定义Hibernate配置和实体类。...

    java防御游戏源码

    游戏的核心是利用Java的图形用户界面(GUI)技术来构建互动的游戏场景,玩家通过控制防御系统来抵御外星飞碟(UFO)对城市的攻击。下面将详细介绍游戏中的关键知识点。 1. **Java GUI**: 游戏界面的实现离不开Java...

    jasentaa:Clojure和ClojureScript的解析器组合器库

    解析器组合器是一种构造解析器的方法,它允许开发者通过组合小的、简单的解析器来构建复杂的解析器,而不是从头编写复杂的解析规则。这种方式极大地提高了代码的可读性和可维护性,使得在处理语言和数据格式解析时...

    java实现的highcharts与ajax结合动态实时获取数据更新图表

    利用jQuery库中的`$.ajax`或`$.getJSON`方法,周期性地发送请求到Java后端,获取最新的数据并更新图表: ```javascript setInterval(function() { $.getJSON('/getData', function(data) { // 更新图表数据 ...

    编译原理_语法分析器

    如果需要实现更多的功能,如处理函数定义、数组、指针等,需要进一步扩展文法文件,并重新生成解析器代码。 总结来说,编译原理中的语法分析器是构建编译器的关键步骤,而JavaCC作为强大的工具,简化了这一过程。...

    java模仿超级玛丽源码(全屏模式)

    这个项目“Java模仿超级玛丽源码(全屏模式)”就是一个利用Java 2D技术实现的经典游戏《超级玛丽》的复刻版。下面我们将深入探讨其中涉及的关键知识点。 1. **Java 2D API** Java 2D API是Java平台的一部分,它...

    ajax+json+java

    【标题】"Ajax + JSON + Java" 是一种常见的前端与后端交互技术组合,用于实现网页的异步数据更新,无需整个页面刷新。Ajax(Asynchronous JavaScript and XML)是利用JavaScript进行局部页面更新的技术,而JSON...

    Java核心技术第8版(pdf+chm)

    理解Class类、Constructor、Method、Field等核心接口,可以实现动态代理、配置文件解析等功能。 8. **模块系统**(Java 9+):Java 9引入了模块系统(Project Jigsaw),通过明确的模块化边界提高了代码的可维护性...

Global site tag (gtag.js) - Google Analytics