昨天講到Antlr尋訪(Visit)模式。這個功能在實務上比較少用到,尤其是在做工具時。
不管是Listener監聽者模式或是尋訪(Visit)模式,基本上Antlr只要把抽象語法樹(abstract syntax tree或者縮寫為AST) 產生出來,它就能在每一個節點停留,產生對應文法檔(.g4)裏的元素的事件。
我們只要在事件裏寫CODE. 就能把一些不可思議的工具做出來。
在官方手冊的第四章,就有一個從Java class 裏,逆產生 interface 宣告的例子。
很多程式員,終其一生不會碰到這個問題。或是只要用別人開發出來的重構(REFACTOR)工具來解決即可。
問題:給定一個Java class,如
import java.util.List;
import java.util.Map;
public class Demo {
void f(int x, String y) { }
int[ ] g(/*no args*/) { return null; }
List<Map<String, Integer>>[] h() { return null; }
}
請產生該CLASS的介面(interface),如
interface IDemo {
void f(int x, String y);
int[ ] g(/*no args*/);
List<Map<String, Integer>>[] h();
}
有些人可能會用正規表達示的來find/replace.
把public class 替換成 interface,
把Demo 替換成IDemo,這時開始有問題了,在{ 左邊是 類別名,....
看起來簡單,做起來有點問題。
如果透過Antlr的Listener監聽者模式,那會如何解呢?
首先編一個Java的文法檔。
!@#$%%%%%,蝦米,這是我等程式員,做的到的事嗎?
現成的Java的文法檔,BNF檔,符合Antlr的(.g4)格式嗎?
幸好,在開放源始碼的精神帶動下,
世界的語言愛好者,自動自發在Parr教授的帶領下,
維護了很多語言的文法檔,
其中也包括Antlr 自己的BNF文法檔。好像是一種遞迴的關係。
或是雞生蛋,蛋生雞的問題,到底是先有Antlr,還是先有文法呢?
標準的JAVA 1.6版文法,有700 多行文法。
以本案需求,只需要對幾個點,如CLASS類別名,裏面的方法名,方法的實作要去掉,但方法的參數要照抄,
把這些方法挑出來,
一樣的指令,$ antlr4 Java.g4
產生對應程式碼,
public interface JavaListener extends ParseTreeListener {
..................
/**
* Enter a parse tree produced by {@link JavaParser#classDeclaration}.
* @param ctx the parse tree
*/
void enterClassDeclaration(JavaParser.ClassDeclarationContext ctx);
/**
* Exit a parse tree produced by {@link JavaParser#classDeclaration}.
* @param ctx the parse tree
*/
void exitClassDeclaration(JavaParser.ClassDeclarationContext ctx);
.........................
............................
.........................
/**
* Enter a parse tree produced by {@link JavaParser#methodDeclaration}.
* @param ctx the parse tree
*/
void enterMethodDeclaration(JavaParser.MethodDeclarationContext ctx);
/**
* Exit a parse tree produced by {@link JavaParser#methodDeclaration}.
* @param ctx the parse tree
*/
void exitMethodDeclaration(JavaParser.MethodDeclarationContext ctx);
...............................................
這是Antlr 從文法檔中產生的CODE, 對應
grammar Java;
........................................
classDeclaration
: 'class' Identifier typeParameters? ('extends' type)?
('implements' typeList)?
classBody
;
.....................................
methodDeclaration
: type Identifier formalParameters ('[' ']')* methodDeclarationRest
| 'void' Identifier formalParameters methodDeclarationRest
;
.....................................
對應來看,可看出 Antlr生成程式的命名規則,就是文法裏有classDeclaration 元素,Antlr
就生成兩個方法,enterClassDeclaration,exitClassDeclaration。
7百多行的JAVA文法檔裏,每個元素都會產生,enterXXXXXX,exitXXXXXX, 從字面上來看,
就是在enter元素、exit元素時,觸發事件。
我們就在事件裏寫CODE.
以本案例,
就寫如下的CODE,
import org.antlr.v4.runtime.TokenStream;
import org.antlr.v4.runtime.misc.Interval;
public class ExtractInterfaceListener extends JavaBaseListener {
JavaParser parser;
public ExtractInterfaceListener(JavaParser parser) {this.parser = parser;}
/** Listen to matches of classDeclaration */
@Override
public void enterClassDeclaration(JavaParser.ClassDeclarationContext ctx){
System.out.println("interface I"+ctx.Identifier()+" {");
}
@Override
public void exitClassDeclaration(JavaParser.ClassDeclarationContext ctx) {
System.out.println("}");
}
/** Listen to matches of methodDeclaration */
@Override
public void enterMethodDeclaration(
JavaParser.MethodDeclarationContext ctx
)
{
// need parser to get tokens
TokenStream tokens = parser.getTokenStream();
String type = "void";
if ( ctx.type()!=null ) {
type = tokens.getText(ctx.type());
}
String args = tokens.getText(ctx.formalParameters());
System.out.println("\t"+type+" "+ctx.Identifier()+args+";");
}
}
我們實作一個ExtractInterfaceListener監聽者 ,擴展JavaBaseListener介面。
覆寫(Override)我們需要的方法(事件)。
一進入(ENTER)到類別宣告時,請產生System.out.println("interface I"+ctx.Identifier()+" {");
程式中寫的很直白。ctx.Identifier()是類別的名稱,前面加上"interface I"…,
文法檔裏的 type, Identifier, formalParameters,這些元素,都可以在程式中被使用。
只要Method 的宣告,就用";"結束,不要{}裏的程式內容。
基本上,Listener監聽者模式,就能實作強大的語言轉換器。
Parr教授累積數十年的語言解析功力,提出一套方法論,
只要文法寫的沒問題,Antlr 就能產生 按此文法 寫出的程式的解析程式碼,
而這工具產生的解析程式碼,基本上是產生抽象文法樹,遍尋所有節點,
Listener監聽每個節點,產生對應的EVENT,喜歡做語言轉換工具者,
就在這些EVENT裏,做轉換。
廣泛的類C語言,彼此之間的轉換,基本上有一半的CODE不用置換。像if , for , 宣告變數的,基本上不用變。
但從類C語言,換其他語言,變動就較大了。
有一些主打能省CODE的行數的新語言,有時候可用這種方式來更充分理解為什麼能省CODE.
當這類工具一產生,把從前只有高手才寫的出來的LEXER/PARSER 手寫CODE.
變得普通程式員可用工具做掉。這個算是時代的進步。對新語言的產生也有推波助瀾的作用。
Antlr 默默的委身在一些爆紅的,劃時代創新專案裏,如ORM的關鍵成型代表:Hibernate.
基本上也是用另一種語言來代替SQL.結果所有的後端框架都要ORM了。沒ORM就不算一個正常的框架,
這是基本配備。