顯示具有 dsl 標籤的文章。 顯示所有文章
顯示具有 dsl 標籤的文章。 顯示所有文章

2014/01/10

Groovy in JVM

Groovy 是必須要依附在 JVM 的 script 語言,他的語法是簡化自 Java 的結果,由於她專為 Java 而生的特性,我們只需要附加幾個 groovy 的 jar 檔,就可以使用 groovy。事實上,我們只需要 groovy 當作是一種 XML,XML 有自己的語法規範,在 JVM 會有 XML Parser 協助轉換 XML 為 Java 的物件,而 groovy 就像是一種進階的 XML 文件檔案,他不單能描述資料,也能跟 Java 物件互動,這也是 groovy DSL 帶來的優勢。

以往我們以 XML 作為延伸 Java 系統的第一選擇,但在這些高階 DSL 的協助下,我們可以用 groovy 更精簡的語法,而得到比 xml 更多的功能。

Installation

安裝有兩個部份,第一個部份是標準元件,首先到 groovy 官方網站下載 套件,目前是 2.2.1 版 groovy-binary-2.2.1.zip,通常處理 Java 的套件,我不會選擇使用 windows installer,而是下載 binary package,然後再自行到環境變數設定 GROOVY_HOME 為 c:\java\groovy-2.2.1(我習慣把套件Java都放在 c:\java 裡面),然後再把 %GROOVY_HOME%\bin 放到 PATH 裡面。Linux 環境也是類似的作法,差別只是 Linux 環境的 script 不一樣,通常可以修改 /etc/profile,然後增加 export GROOVY_HOME=/usr/share/groovy-2.2.1 ,增加 export PATH=$GROOVY_HOME/bin:$PATH 。

第二個部份,是安裝 IDE 的 Plugin ,在 Groovy-Eclipse 的 網頁 裡面,選擇適合自己 Eclipse 版本的 Plugin,我是選擇 4.3(Kepler) ,在 Eclipse -> Help -> Install New Software 裡面把 groovy-eclipse 的 http://dist.springsource.org/release/GRECLIPSE/e4.3/ 路徑加進去,就可以安裝Groovy-Eclipse了。

Groovy with Eclipse - Tutorial 給了一篇 tutorial,我覺得作者每一小段都提供簡單的 sample 的方法還不錯,以下我們用類似的方式測試,但我修改了一些 topic 的先後順序。

source code

如果需要以下內容的範例程式,請點擊 連結 下載檔案。

HelloWorld

這是一個列印HelloWorld的範例,前面是用 groovy 做的,檔名為 TestGroovy.groovy,後面是 TestJava.java,兩個範例的功能一樣,但精簡的 groovy 帶來了有效率的開發。

package test.groovy

class TestGroovy {

    static main(args) {
        def mylist=[
            "字串  Hello",
            " World",
            "...",
            ", 數字:",
            1
        ]
        mylist.each{ print it }
        println ""
    }
}
package test.groovy;

import java.util.ArrayList;
import java.util.List;

public class TestJava {
    public static void main(String[] args) {
        List<String> mylist =new ArrayList<String>();
        mylist.add("字串  Hello");
        mylist.add(" World");
        mylist.add("...");
        mylist.add(", 數字:");
        mylist.add(""+1);

        for(String a: mylist) {
            System.out.print(a);
        }
        System.out.println("");
    }
}

跟Java的差別

Difference with Java 裡面提到了跟 Java 程式碼的差異

  1. Floating point number literals 預設的型別為 BigDecimals
  2. 預設就會 import java.io., java.lang., java.math.BigDecimal, java.math.BigInteger, java.net., java.util., groovy.lang., groovy.util.
  3. == 就等同於 Java 裡的 equals,如果要判斷是不是同一個instance,就要寫成 foo.is(bar)
  4. in 是 keyword
  5. 宣告 array 要寫成 int[] a = [1,2,3] ,而不是 int[] a = {1,2,3};
  6. loop迴圈要寫成 for (i in 0..len-1) {...} 或是 for (i in 0..<len) {...} 或是 len.times {...} ,而不是 for (int i=0; i < len; i++) {...}
  7. Semicolons 句末的分號; are optional
  8. return keyword is optional
  9. static method 裡面可使用 this
  10. Methods and classes 預設為 public
  11. method 拼錯了,只會在 runtime 產生 MissingMethodException,而不會在 compile time 發生,這要參考 Runtime vs Compile time, Static vs Dynamic 這篇文章。在 groovy 2.0.0 之後,支援了 @groovy.transform.TypeChecked 這個 annotation,可在 compile time 提供錯誤檢查。

precompile or direct mode

執行 TestGroovy.groovy 有兩種方式,Groovy簡介、安裝SDK及基本開發環境 有一張說明圖寫得很清楚。

在 Eclipse 的 Run As 裡面執行 TestGroovy.groovy 也會發現有兩種選擇,一種是 Groovy Console,一種是 Groovy Script,選第一種時,會出現一個獨立的 Groovy Console IDE 視窗,可直接在視窗裡面修改 groovy code,然後直接執行,這也就是 direct mode,而選擇 Groovy Script 則是在 Eclipse Console 裡面直接得到執行結果,查看一下會發現,TestGroovy.groovy 會先被編譯成 TestGroovy.class 與 TestGroovy$_main_closure1.class,並放在 bin 目錄裡面,Eclipse console 是使用 JVM 執行這些 Java class 的結果。

在一開始獨立安裝的 groovy-binary-2.2.1.zip 裡面,我們可以在 bin 目錄找到幾個 batch 檔,groovyConsole.bat 就是剛剛 Groovy Console IDE 視窗,文件在 Groovy Console 。 groovyc.bat 可以把 TestGroovy.groovy 編譯成 java class 檔,groovy.bat 則
可以執行 groovy script,但這個並不是執行 groovyc 編譯的 class 檔,在Using Groovy from the command line 得到相關的文件說明。

>groovy -c UTF-8 test\groovy\TestGroovy
字串  Hellolo World..., 數字:r:1

Groovy Classes, Objects and Methods

Groovy class 預設為 public,所有的 groovy source file 都是以 .groovy 作為副檔名。

// 檔案目錄 test\groovy\SumIt.groovy

package test.groovy

class SumIt {

    static sum(a,b) {
        a+b
    }
    static main(args) {
        println sum(1,5)
        println sum(2,4)
    }
}

groovy 的欄位預設為 private,且會自動產生 getter與 setter methods,所以可以直接使用 setFirstName,也可以用 p.lastName 的方式,存取欄位資料。

當我們用 new Person(firstName: "名字", lastName:"姓") 這個方式產生 Person 時,groovy將會自動產生含有這兩個欄位作為參數的 constructor。

使用 == 就等同於在 Java 的 equals,在 Person 類別必須要覆寫 hashCode 與 equals 才有作用。

// test\groovy\Person.groovy

package test.groovy

public class Person{
    String firstName
    String lastName
    int age
    def address

    static void main(def args) {
        Person p = new Person()
        // use the generated access methods
        p.setFirstName("Name")
        // This will still use the generated access method, it is not a direct access!
        p.lastName = "LastName"
        p.age=10;
        p.address = ("Homestreet 3");
        println(p.firstName + " " + p.lastName);

        // use the generated constructor
        Person p2 = new Person(firstName: "名字", lastName:"姓");
        println(p2.firstName + " " + p2.lastName);

        println ""
        println "p==p2 : "+(p==p2);
        println "p.is(p2) : "+p.is(p2);
        p2=p;
        println ""
        println("after p2=p")
        println "p==p2 : "+(p==p2);
        println "p.is(p2) : "+p.is(p2);

        Person p3 = new Person();
        p3.setFirstName("Name");
        p3.age=10;
        p3.lastName = "LastName";
        p3.address = ("Homestreet 3");

        println ""
        println "p==p3 : "+(p==p3);
        println "p.is(p3) : "+(p.is(p3));
    }

    boolean equals(o) {
        if (this.is(o)) return true;

        if (!o || getClass() != o.class) return false;

        Person that = (Person) o;

        if (firstName? !firstName.equals(that.firstName) : that.firstName!= null) return false;
        if (lastName? !lastName.equals(that.lastName) : that.lastName!= null) return false;
        if (age? !age.equals(that.age) : that.age!= null) return false;
        if (address? !address.equals(that.address) : that.address!= null) return false;

        return true;
    }

    int hashCode() {
        int result = (firstName ? firstName.hashCode() : 0);
        result = 31 * result + (lastName ? lastName.hashCode() : 0);
        result = 31 * result + (age ? age.hashCode() : 0);
        return result = 31 * result + (address ? address.hashCode() : 0);
    }
}

執行的結果為

Name LastName
名字 姓

p==p2 : false
p.is(p2) : false

after p2=p
p==p2 : true
p.is(p2) : true

p==p3 : true
p.is(p3) : false

Groovy 允許使用不固定個數的參數,但該optional 參數,必須要指定預設值 c=10。

// test\groovy\SumIt.groovy
package test.groovy

class SumIt {

    static sum(a,b, c=10) {
        a+b+c
    }
    static main(args) {
        println sum(1,5)
        println sum(2,4)

        println sum(2,4,5)
    }
}

Loops

迴圈可使用 list.each 的方式,搭配指定變數 firstName-> println firstName 或是使用內建的 it 變數 println it。

針對 number 變數,有 upto(), downto(), times(),也可以使用 (1..6) 這樣的 range data type,來處理迴圈。

package test.groovy

class PrintLoop {

    public static void main(def args){
        def list = [
            "John",
            "Mary",
            "James"
        ]

        // using a variable assignment
        list.each{firstName-> println firstName }
        // using the it variable
        list.each{println it}

        println ""
        5.times {println "Times + $it "}
        1.upto(3) {println "Up + $it "}
        4.downto(1) {print "Down + $it "}

        def sum = 0
        1.upto(100) {sum += 1}
        print sum

        (1..6).each {print "Range $it"}
    }
}

執行結果為

John
Mary
James
John
Mary
James

Times + 0 
Times + 1 
Times + 2 
Times + 3 
Times + 4 
Up + 1 
Up + 2 
Up + 3 
Down + 4 Down + 3 Down + 2 Down + 1 100Range 1Range 2Range 3Range 4Range 5Range 6

Groovy Data Types

Reference variables

Groovy 的所有變數都是 Reference variables,沒有 primitive variables,即使寫成 int 10,背後還是轉換為物件。

Groovy 允許使用 static and dynamic typed variables,動態資料型別要以 def 作為 data type。

以下的 TypeTest可用來查看 def 型別轉換的狀況。

package test.groovy

class TypesTest {

    static main(args) {
        int i = 1 // Short form for Integer i = new Integer(1)
        int j = i +3
        int k = i.plus(3); // Same as above
        // Make sure this worked
        assert(k==4);
        println i.getClass().getName()
        println j.getClass().getName()
        println k.getClass().getName()

        // Automatic type assignement
        def value = 1.0F
        println value.getClass().getName()
        def value2 = 1;
        println value2.getClass().getName()
        value2 = value2 / 2;
        println value2
        println value2.getClass().getName()
    }
}

執行的結果

java.lang.Integer
java.lang.Integer
java.lang.Integer
java.lang.Float
java.lang.Integer
0.5
java.math.BigDecimal

strings

Groovy 的 string 有兩種,第一種是 " " 包圍的 string,型別為 org.codehaus.groovy.runtime.GStringImpl,這種字串稱為 Groovy String,簡稱 GString,可以直接用 $name 進行替換的運算,第二種是 ' ' 包圍的 string,這就是 Java 的字串,型別為 java.lang.String。

package test.groovy

class StringTest {
    static main(args) {
        def name = "John"
        def s1 = "Hello $name" // $name will be replaced
        def s2 = 'Hello $name' // $name will not be replaced
        println s1
        println s2
        println s1.getClass().getName();
        println s2.getClass().getName();
    }
}

執行結果

Hello John
Hello $name
org.codehaus.groovy.runtime.GStringImpl
java.lang.String

Lists and maps

以 List persons = list[] 定義新的 list,persons.get(i) or persons[i] 可取得第 i 個元素。Map是以 Map[key1:value1, key2:value2,...]的方式定義,可以直接指定某一個位置的內容 map[key]=value。

package test.groovy

class ListMap {
    static main(args) {
        List<Integer> list = [1, 2, 3, 4]
        println list[0]
        println list[1]
        println list[2]

        List<Person> persons = list[]
        Person p = new Person(firstName: "John", lastName:"LastName")
        persons[0] = p
        println persons.size()
        println persons[0].firstName
        println persons.get(0).firstName

        Map map = [:]
        def map2 = ["John":"LastName", "James":"LastName2"]
        println map2["James"]
        map2["Test"] = "Tester"
        println map2["Test"]
    }
}

Ranges

只要Java物件有實作 previous() and next() 這兩個method,且有 implements java.lang.Comparable,都可以用在 Ranges。 0..4 就是 0,1,2,3,4 而 0..<4 就是 0,1,2,3。

package test.groovy

class RangesTest {
    public static void main(args){
        for (i in 0..4) {
            println ("Hello $i")
        }

        for (i in 0..<4) {
            println ("2nd Hello $i")
        }
    }
}

Regular Expression

運算子 說明
=~ Find: 如果pattern在字串中有出現
==~ Match: Pattern必須完整符合整個字串
~String 將String轉換為 regular expression
package test.groovy

class RegularExpressionTest {
    public static void main(String[] args) {
        // Defines a string with special signs
        def text = "James happily ever after"

        // Every word must be followed by a nonword character
        // Match
        if (text==~/(\w*\W+)*/){
            println "Match was successful"
        } else {
            println "Match was not successful"
        }
        // Every word must be followed by a nonword character
        // Find
        if (text=~/(\w*\W+)*/){
            println "Find was successful"
        } else {
            println "Find was not successful"
        }

        if (text==~/^J.*/){
            println "There was a match"
        } else {
            println "No match found"
        }
        def newText = text.replaceAll(/\w+/, "hubba")
        println newText
    }
}

Closures

closure 是可使用在 class 或 method 外面的一段程式碼,語法為{para1, para2 -> code of the closure},para1與para2為後面程式碼的參數。list.each裡面可以使用closure。

package test.groovy

class ClosureTest {
    public static void main(args){
        List<Integer> list = [5, 6, 7, 8]
        list.each({line -> println line})
        list.each({println it})
    }
}

Meta Object Protocol

meta object protocol 可動態在runtime增加 methods與 properties。如果程式中,呼叫了沒有定義的 method/property,就會自動呼叫下列這些 method。

  1. def methodMissing (String name, args) - Called for missing method
  2. void setProperty (String property, Object o) - called for non existing setter of a property
  3. Object getProperty (String property) - called for non existing getter of a property
package test.groovy

class MetaObject {
    def map

    Object getProperty (String property){
        println "Setting this propery"
        return 5;
    }

    void setProperty (String property, Object o){
        println "Hallo"
    }

    def methodMissing (String name, args){
        def s = name.toUpperCase();
        if (s.startsWith("HELLO")) {
            println "This method stats with Hello. Full name $name"
        } else {
            println "This method is missing"
        }
    }

    public static void main (args){
        def test = new MetaObject();
        test.hall();
        test.helloMethod();
        test.Hallo();
        test.test= 5;
        println test.test;
    }
}

Operator overloading

groovy支援在class裡面覆寫一些標準的 operations,例如 a+b 可以透過實作 plus method 調整 a+b 的作用。

Operator 名稱 Method
a+b plus a.plus(b)
a-b minus a.minus(b)
a*b star a.multiply(b)
a/b divide a.div(b)
a%b modulo a.mod(b)
a--, --a decrement a.previous()
a++, ++a increment a.next()
a**b power a.power(b)
a-b minus a.minus(b)
a-b minus a.minus(b)

File IO

將 userlist.txt 檔案內容,加上行號,填寫到 userlist2.txt 裡面

package test.groovy

class FileTest {
    static main(def args){
        File file = new File("d:/temp/userlist.txt")
        File file2 = new File("d:/temp/userlist2.txt")
        if( !file2.exists() ) {
            file2.createNewFile() //if it doesn't already exist
        }
        assert file2.exists()
        def file2writer = file2.newWriter()
        file2writer<<"";

        def lineNumber = 0;

        file.eachLine{ line ->
            lineNumber++
            file2 << "$lineNumber: $line\n"
        }
    }
}

XML

使用 XmlParser 處理 XML 文件

package test.groovy

class XMLTest {
    static void main(args){
        def xmldocument = '''
    <persons> 
      <person><firstname age="33">John</firstname><lastname>Chen</lastname></person>
      <person><firstname age="34">James</firstname><lastname>Lee</lastname></person>
    </persons>
    '''
        def persons = new XmlParser().parseText(xmldocument);
        def allRecords = persons.person.size()
        println("Number of person is: $allRecords")
        def person = persons.person[0]
        // name is the name of the XML tag
        println("Name of the person tag is:" + person.name())
        // text gets the text of the node firstname
        println(person.firstname.text())

        // Lets print out all important information
        for (p in persons.person){
            println "${p.firstname.text()} ${p.lastname.text()}"
        }
    }
}

Thread and Concurrency

groovy 跟 java 一樣,可以使用 thread 跟 synchronzied block。

package test.groovy
import java.util.concurrent.atomic.AtomicInteger

class SynchronizedTest {
    private final myLock = new Object()

    static synchronized void greet() {
        println "world"
    }

    synchronized int answerToEverything() {
        return 42
    }

    void foo() {
        synchronized ("myLock") {
            println "bar"
        }
    }

    private def counter = new AtomicInteger()
    synchronized out(message) {
        println(message)
    }

    void processthread() {
        def th = Thread.start {
            for( i in 1..8 ) {
                sleep 30
                out "thread loop $i"
                counter.incrementAndGet()
            }
        }

        for( j in 1..4 ) {
            sleep 50
            out "main loop $j"
            counter.incrementAndGet()
        }
        th.join()

        assert counter.get() == 12
        out "result:"+counter.get();
    }

    static main(args) {
        SynchronizedTest test=new SynchronizedTest();
        test.processthread();
    }
}

另外還有 Groovy GPars 支援 Actors, Map/Reduce, Dataflow, Fork/Join 這些平行處理的功能。

NullPointerException

groovy 可以用 ?: 或是 ?. 來處理 java 最常遇到的 NullPointerException。

package test.groovy

class AvoidNullPointer {
    static void main(args){
        Person user = null;
        def firstName = user?.firstName;
        println "firstName:"+firstName

        user = new Person();
        def firstName2 = user?.firstName;
        println "firstName2:"+firstName2

        def user2
        user2 ?: new Person();
        def firstName3 = user?.firstName;
        println "firstName3:"+firstName3
    }
}

Java class 跟 groovy script 的互動

我們在 Person.groovy 定義的Person物件,可以直接在TestJava.java 裡面直接使用 Person。

// test\groovy\Person.groovy
// test\groovy\TestJava.java

package test.groovy;

public class TestJava {
    public static void main(String[] args) {

        Person p = new Person();
        p.setFirstName("Firstname");
        System.out.println("p="+p.getFirstName());
    }
}

結語

groovy 就等同於一個簡化 Java 語法的內建動態語言,她必須跟 JVM 共生,嵌入 JVM 雖代表她天生就受限於 JVM,但也表示我們可以很容易地以Java方式整合 groovy。

2014/01/05

DSL in Action - written by Debasish Ghosh

看過 Martin Fowler 在 InfoQ 的演講影片 Introduction to Domain Specific Languages之後,接下來,選擇看一本 DSL in Action 的書,這本書的內容涵蓋 JVM 所能支援的 DSL,並從各種角度,去分析實現 DSL 方法的優劣。這本書分兩個部份,第一個部份是 1~3 章,再加上 Appendix A,第二個部份則是 DSL 的實作範例,接下來先看第一個部份。

DSL

DSL 的功能是將 problem domain 對應到 solution domain,首先要找出兩個 domain 之間的共通語彙(vacabulary),透過這個共通的專有名詞,來建立 domain expert 跟 IT system 之間的溝通模型。換句話說,就是要從 domain expert 所描述的業務邏輯中,找到該 domain 的專有名詞、專用術語,再以這個專用術語為基礎,建立溝通互動的語言,形成雙方都能使用的 DSL。

Programmer 設計 DSL 時,要注意幾個地方:

  1. 要一直把使用者放在心上,因為 DSL 的好壞,不好用的話,會直接影響到使用的意願,那就失去了設計 DSL 的原意,沒有人用的 DSL 就是個失敗的設計。

  2. DSL 只需要針對該業務範圍進行抽象化的設計,沒有多餘的東西,太複雜冗長的語言,只會講低使用者的使用意願。

DSL 分為 Inernal、External、非文本DSL 三種。

DSL 的優缺點

DSL 的優點很容易理解,因為適度的抽象化,可幫助使用者更容易處理複雜的問題,試想,如果沒有 SQL ,那麼我們應該怎麼操作 Relational DB,要直接用 API 下 select() 嗎?

除了優點之外,我們更要注意 DSL 的缺點

  1. 設計DSL是很困難的工作
  2. DSL 需要大量前期設計,投入的人力成本
  3. DSL 增加的中間層,可能會有性能憂慮
  4. DSL 有時缺少足夠的編輯工具
  5. 可能會造成「學不完的DSL」現象
  6. DSL 可能導致語言之間的摩擦:開發APP可能需要同時使用多個 DSL,也因此可能造成整合上的問題

良好的抽象應具有的特質

DSL 牽涉到簡化業務邏輯的設計,這是一種抽象化的過程,我們要知道,要從哪些指標判斷抽象化的優劣

  1. 極簡:只開放使用者需要使用的功能,沒有多餘、外露的內部實作內。例如 API 要回傳 Map 而不是 TreeMap,可使用繼承的方式,隱藏內部的實作設計。
  2. 精煉:抽象化的內容不包含任何非本質的細節,移除不必要的細節,可利用 DI(Dependency Injection)隱藏實現的細節
  3. 擴展性:抽象設計可在不影響現有使用者的情況下,持續改進升級。利用 mixin、functional programming 的 closure、open class 的方式達到擴展性
  4. 組合性:可與其他抽象設計組合成更高階的抽象設計。使用 Command、Decorator Pattern。但可能會在multithread環境下造成問題。

實作 DSL 的方法

第二章一開始,以一個證券交易的實例設計 DSL,第一個版本是用 Java語法,但遇到交易員不熟悉 Java 語法的問題,而且 Java 語言裡有過多跟證券交易無關的東西。第二個版本是 XML,XML適合描述文件結構,不適合拿來作為 DSL,而且XML有太多無關的標記。因此又有了第三個版本,是使用 Groovy,這個版本才勉強有了個適當的 DSL。

Internal DSL 的分類

  1. 生成式:編譯後,轉換生成實作語言的的code

    1.1 編譯時meta programming:Lisp, Template Haskell

    1.2 執行時meta programming:Ruby, Groovy

  2. 內嵌式:領域專用的類別內嵌於宿主語言的類別系統

    2.1 Smart API:Java, Ruby

    2.2 AST:Java, Ruby, Groovy

    2.3 內嵌類別:Haskell, Scala

    2.4 反射式meta programming:Ruby, Groovy

External DSL 的分類

  1. 上下文驅動的字串操作

  2. xml 轉換成可使用的資源

  3. DSL 工作台

  4. DSL 中內嵌異質代碼

  5. 基於解析器組合子的 DSL 設計

實作 DSL 的方法有很多,我們應該如何選擇一個最適當的方法呢?要考慮的因素如下:

  1. 重用現有的機制:利用強大的宿主語言提供的功能,例如 Scala或 Haskell
  2. 充分利用現有的知識:要根據開發團隊現有的知識水準來選擇實作的方法。
  3. 外部DSL的學習曲線:外部DSL可能會很複雜,必須要把學習曲線納入開發成本。
  4. 適當的表現力:Internal DSL有重用宿主語言的優勢,但相對也約束了描述業務領域的表現力
  5. 組合性:DSL跟宿主語言之間的是不是能簡單地整合起來

DSL Driven Application Development

在開發 JVM 環境的 DSL 時,最重要的就是要看怎麼跟 JVM 整合在一起,開發的時候,要注意三個問題:1. 整合問題 2. 異常與錯誤的處理 3. 性能的表現。

如果要在一個系統裡面,同時使用多個 DSL,對於 JVM 來說,整合是不成問題的,而且我們可以選擇使用 Java、Groovy、Spring、JRuby、Scala 這些語言來實作 DSL,直接分別將這些實作包裝成 jar,就可以讓主程式引用了。

Internal DSL: Groovy

如果選擇使用了 Groovy,有兩種方式可以將 Groovy Script 整合起來:

  1. 使用 javax.script 的 Script Engine,Script Engine 是一種 sandbox,Groovy DSL 跟 Java Class 之間無法互通,另外在出現 Exception 的時候,stack trace 顯示的行號沒辦法直接對應到 DSL 中的行號,不容易除錯。所以在整合 Groovy DSL時,要優先考慮使用第二種方法。

  2. 使用 groovy.lang.GroovyClassLoader,以 Groovy 實做的 Order 類別,可直接讓 JVM 使用,在運作 dsl script 之後,也可以直接回傳 Order 的 List,Groovy 跟 Java 之間的互動比 Script Engine 的方法整合地更緊密。

Internal DSL: Spring

Spring 2.0 版之後就支援使用 Ruby、Groovy 實作的 Bean,還能直接運用 Spring 的 DI 功能,動態注入 script code,下面是一個定義 bean 的範例,透過 refresh-check-delay 的設定,spring 將會在每5000毫秒檢查script是否有被更新,而自動 refresh 載入的 bean,系統就可以在不關機的條件下,直接更新業務邏輯。

<lang:jruby
    id="accIntCalcRule"
    refresh-check-delay="5000"
    script-interface="org.spingframework.scripting.AccruedInterestCalculationRule"
    script-source="classpath:RubyAccruedInterestCalculationRule.rb">
</lang:jruby>

External DSL: XML

使用 XML Parser 進行文件的解析與處理。

External DSL: ANTLR、JAVACC

ANTLR(Another Tool for Language Recognition)裡面包括了 詞法分析器(Lexer)、語法分析器(Parser)、樹分析器 (tree parser) 的功能,編寫文法(詞法規則與語法規則)描述文件之後,交給ANTLR,就能生成以 Java 語言實作的 parser 程式碼,ANTLR 3.X支援生成 Java,C#,JavaScript,C 這幾種語言的程式碼。

感想

會去看這本 DSL in Action 根本是個意外,原本是要了解 Gradle,然後知道了 Groovy,漸漸地 dig in,最後到了 DSL。讀了幾篇文章後,才了解到,我們在設計系統時,考慮的系統設定模組、外部 API 模組,其實都屬於一種 DSL,我們也可以在設計系統設定時,採用不同於 XML 的方法,設計 API,也可以考慮使用 Groovy實作。而且看了之後,才知道還有太多專有名詞還不了解,現在頂多只能懂得一些皮毛。

以沒寫過 Groovy, Ruby,更別說會寫 Haskell, Scala的狀況,要能深刻了解這本書的內容,還真的有些吃力。接下來,應該把注意力先放回到Groovy。

2013/12/30

DSL(Domain Specific language) vs GPL(General Purpose Language)

DSL 不是 Digital Subscriber Line,也跟家裡面 ADSL 沒有關係,而是 Domain Specific language 的縮寫。GPL 也不是 General Public License,而是 General Purpose Language 的縮寫。

專有名詞的縮寫

學習專業知識,一開始都會被專業的縮寫搞亂,也要能將這些專有名詞縮寫脫口而出,才能嚇唬人,突顯自己的專業。DSL 不是比較常聽到的 Digital Subscriber Line,而是 Domain-specific language 中文有些人翻譯為領域特定語言,但依照中文的習慣,要念成特定領域語言,為了遵循英文原文的順序,還是應該改成「領域專用語言」比較順口。

跟 DSL 對應的語言為 GPL,GPL 也不是常常聽到的 General Public License ,而是 General Purpose Language 的縮寫,舉個簡單的例子來說,Java、C 都屬於 GPL,而 HTML、SQL、MathML、Unix Shell Script、groovy 等等語言都算是 DSL。

其實大家最常遇到的,最常見的,會是各式各樣的 DSL,而不是 GPL,嚴格地來說,沒有一種語言會是 GPL,形成某一種語言之後,就會馬上變成是 DSL了,因為總會有某一些應用,是無法簡單地用 Java、C 這樣的 GPL 所描述的,那究竟怎麼判斷,那一種語言是屬於 GPL 呢?

GPL 如何分類?

開發人員的逆襲: Domain-Specific Languages 裡面提到:Karl Frank 認為,DSL泛指任何特定領域的語言,甚至連 C#、Java 都算是特定領域的語言,因為它們都是針對特定目的(軟體開發),用於特定場合的語言。就軟體開發這塊領域而言,C#、Java 可運用於各類型的軟體開發,所以我們通常將它們視為GPL。

但其實在分辨 GPL 的時候,必須還要再加上子分類,必須先知道是那一種類別的 GPL,從 Is UML a domain specific language (DSL)? 的討論得知,當我們在討論 Programming Language 的時候,C、Java 就是一種 General Purpsose Programming Language,而討論 Modeling Langage 的時候,UML就是一種 General Purpose Modeling Language,而 XML 是一種 General Purpose Markup Language。

Java 跟 Groovy 的關係,就像是 Erlang 跟 Erlang OTP 的關係一樣,Erlang 可類比 Java,而 OTP 並不屬於 GPL,他是一種 DSL,適用的領域是 high concurrency 與 distributed 的伺服器運算環境。

Compiler

講到程式語言,就馬上會回想起十幾年前學習的 Compiler,當時老師教的是 Lex(Lexical Analyzar) 與 Yacc(Yet Another Compiler Compiler) ,雖然是有學過,也可以很順口地落出 lex and yacc,但也僅只於此,再深入的東西也都忘光沒有了。你可以自行參閱 Lex 與 Yacc 介紹以lex/yacc實作算式計算機

如果想要做個新的語言編譯器,a list of compiler books — 汗牛充棟的編譯器參考資料 列出了一堆相關書籍,但我想我應該不會跳進那個大坑洞裡。

在 Java 領域中,就必須要認識 ANTLR,ANTLR 是 ANother Tool for Language Recognition 的縮寫,從 使用 Antlr 開發領域語言 得知,ANTLR 是用來開發一種 DSL 的工具。

大概的過程就是先撰寫語法定義 Compiler.g,定義好語法之後,用 ANTLR 產生詞法分析器和語法分析器,使用這個分析器,就可以用來驗證,我們輸入的語言expression,到底有沒有符合這個語言的定義規則,ANTLR支援,可以將分析器生成為 Java,C#,C,Python,JavaScript 等多種語言。

另外還有個工具 Antlrworks,可將 .g 的語法定義檔案,用視覺化的方式呈現出來。

為什麼要了解 DSL?

當你進入 IT 的領域,想要學習使用關聯式資料庫時,別人就會建議你去學SQL,想要當個 SE,就會建議你要學習 shell script,想要學習網頁程式設計,就會建議要學習 Javascript、HTML、CSS 等等東西,每一種語言都是一種工具,是前人為了特定的目的,而定義出來的一種特殊的語言。

重要的是,當我們學習某一種語言的時候,要很清楚的知道,這個語言的使用範圍跟目的。雖然也會有意外的創新發生,例如原本設定在網頁上使用的 JavaScript,轉身一變,成了運作在 nodejs 之下的一種熱門的 Server Side 語言。

DSL 的目標是要為了解決商務業務邏輯與特殊應用領域,跟GPL之間的距離,對於系統設計者來說,先了解 DSL 的理念與技術,才能以最適當的方式,設計出一套最適合的 DSL 語言,當然,所有的設計,都必須符合「恰當」的要求,不能為了設計而設計,以免變成了畫蛇添足,甚至是增加負擔的 DSL。

2013/12/25

DSL - Domain Specific Language 簡介

DSL - Domain Specific Language 簡介

Martin Fowler 針對 DSL 的主題,寫了一本書 Domain Specific Language(有篇文章提供了讀後心得:領域特定語言,被忽視多年的編程利器),另外也有一篇短文 Language Workbenches: The Killer-App for Domain Specific Languages?,根據這篇文章,在 InfoQ 還有一個演講影片 Introduction to Domain Specific Languages

以下以自己的理解,來解釋 Martin Fowler 想要說明的東西。

Language Oriented Programming

DSL 並不一種嶄新的概念,Martin Fowler 認為的新趨勢是 Language Oriented Programming,也就是要用設計一個新的 DSL 的想法,嘗試去做一個更具有彈性的程式實作,他先以一個例子來說明 DSL 的發展過程。

  1. 一開始因為有一份 Evet Data 純文字文件,它有特定的格式,因此產生了撰寫程式處理這個文字資料的需求。

    SVCLFOWLER 10101MS0120050313.........................
    SVCLHOHPE  10201DX0320050315........................
  2. 接下來他用了一個 class diagram,strategy pattern 說明他寫的Java處理程式,基本的想法,就是根據 Data 前面四個字元的識別 keyword,來判斷要使用那一種 strategy,以處理某種特定的Event。

    public void Configure(Reader target) {
     target.AddStrategy(ConfigureServiceCall());
     target.AddStrategy(ConfigureUsage());
    }
    private ReaderStrategy ConfigureServiceCall() {
     ReaderStrategy result = new ReaderStrategy("SVCL", typeof (ServiceCall));
     result.AddFieldExtractor(4, 18, "CustomerName");
     result.AddFieldExtractor(19, 23, "CustomerID");
     result.AddFieldExtractor(24, 27, "CallTypeCode");
     result.AddFieldExtractor(28, 35, "DateOfCallString");
     return result;
    }
  3. 後來他發現,可以將程式抽象化,拆分成兩個部份,就是用固定的程式碼,搭配一個 XML 設定檔。可以用某個 XML Reader 將設定讀進來,然後再對應到上面那個步驟的 strategy class,這樣的好處是,不需要重新編譯程式碼,改寫設定,就可以直接執行。

    <ReaderConfiguration>
     <Mapping Code = "SVCL" TargetClass = "dsl.ServiceCall">
         <Field name = "CustomerName" start = "4" end = "18"/>
         <Field name = "CustomerID" start = "19" end = "23"/>
         <Field name = "CallTypeCode" start = "24" end = "27"/>
         <Field name = "DateOfCallString" start = "28" end = "35"/>
     </Mapping>
    </ReaderConfiguration>
  4. XML 的標籤看起來太礙眼了,可以用另一種更簡潔的語法,來描述這個設定。

    mapping SVCL dsl.ServiceCall
     4-18: CustomerName
     19-23: CustomerID
     24-27: CallTypeCode
     28-35: DateOfCallString
  5. 最後,我們可用另一種自訂的 syntax 語法來改寫,因為要是一種語言,就必須要符合 EBNF 的定義規則。

    mapping('SVCL', ServiceCall) do
     extract 4..18, 'customer_name'
     extract 19..23, 'customer_ID'
     extract 24..27, 'call_type_code'
     extract 28..35, 'date_of_call_string'
    end

Language Oriented Programming 就是一種開發模式,嘗試用創造與使用 DSL 的方式,來完成一個軟體系統。基本上,所有用 DSL 設計的系統,都可以用 GSL 的方式完成。

Internal vs External DSL

接下來,Martin Fowler 說明了 Inernal 跟 External DSL 的差異。

他問了一個問題:以下這個設定的程式碼,是不是一種 DSL?

public void Configure(Reader target) {
    target.AddStrategy(ConfigureServiceCall());
    target.AddStrategy(ConfigureUsage());
}
private ReaderStrategy ConfigureServiceCall() {
    ReaderStrategy result = new ReaderStrategy("SVCL", typeof (ServiceCall));
    result.AddFieldExtractor(4, 18, "CustomerName");
    result.AddFieldExtractor(19, 23, "CustomerID");
    result.AddFieldExtractor(24, 27, "CallTypeCode");
    result.AddFieldExtractor(28, 35, "DateOfCallString");
    return result;
}

答案要根據你在那一種 GSL 上運作,上面這些 code ,如果放到 Ruby,那就是 DSL,但如果在 Java,就不是 DSL,因為 Java 本身並沒有提供任何語言的擴充工具,讓我們可以直接處理這樣的 Configuration Code。

External DSL

  1. 跟運行環境的程式語言完全不同
  2. 需要有 compiler/interpreter 才能運作

缺點:

  1. 缺少IDE
  2. parser/generator 技術太複雜
  3. 自創的語法,產生了很多語言,會出現學習上的困擾及 gap,雖然用Inernal的方式,不需要學很多語言,但還是要學習使用特定的 API

Internal DSL

  1. 以運行環境的程式語言撰寫
  2. 直接使用該程式語言的 syntax

缺點:

  1. 跟運行環境的程式語言緊密結合在一起,例如 因為 ruby 語法的限制 4..18 就不能寫成 4-18
  2. Java, C# 這種 mainstream languages,很難做出 Ineternal DSL,因為當初設計時,就不支援 (不過Java7已經支援 scripting 了)

跳脫 XML,將視野放在 DSL

Martin Fowler 提醒我們,多數的 Java Programmer 會使用 XML,會知道怎麼製作設定檔,但是不知道其實設定檔,也可以再往前推進,形成一種 DSL,下一次當我們面對這麼多繁雜的設定檔時,或許該換個角度想想,是不是能用某種 script,設計出一種領域專用的 DSL 來解決問題。