Domain-Specific Language (DSL)

前言

過去幾年蠻熱門的話題(?),原本還想來學院派式的追本溯源一下,不過凡事起頭難,手邊資料不多,很多論文又不能下載,就作罷了,當閒聊了,又不是要寫論文 XD

我想是因為自己很晚才接觸資訊這個領域,加上上個世紀,網路、社群並非如此盛行,總有種錯覺:這幾年許多新語言的出現不再宣稱自己能解決所有的問題、可以被應用在所有領域,而是將焦點專注於特定問題、領域(domain)的,當然,這不是個新概念,但 Domain-specific language 卻是這幾年才流行起來的。

Generative Programming

Generative Programming 一書是這麼描述 DSL 的:

A domain-specific language (DSL) is a specialized, problem-oriented language. …(略) ... Domain-specific languages can be textural (e.g. SQL) or graphical (e.g. the graphical specification of a GUI in a GUI builder).

哈,基本上這是個很寬鬆的描述,會被 reviewer 打槍的那種,不過不難想像到 DSL 概念的成品已經充斥著我們生活周遭,像是:

  1. lex & yacc:大學 compiler 課用會到!
  2. SQL
  3. 曾經 SLIM 想用的 XUL ,到後來自己設計的 component script
  4. SMP 的 SREAM script
  5. ChucK (那有沒有 SCREAM 呢!)
  6. 微軟 .Net 新一代的 GUI 平台用到的 XAML(很多 open source GUI framework 也有類似的概念)
  7. UML 中的 OCL

這幾年 VM 這種結合 compiler, interpreter 特性的平台語言很是盛行,透過重複利用 VM 的中間碼可以讓原本設計一個 programming langauge 需要的繁雜後端工作得到部份減輕。所以許多研究、實驗性質的 DSL 會考慮建構在 VM 之上。舉例來說,就有許多語言建構在 JVM 之上,參考這邊

優點

那到底 DSL 的好處是什麼呢?wikiDomain-Specific Language: An Annotated Bibliography 做了很好的條列和解釋,不過我還是想再說一遍:( -_-||唉,我想我個性不太好)

  1. Expressive:我想這是最重要一點,人類的思考會受到語言、符號、圖像的限制。因此 DSL 在描述問題、解決問題時都會比 general purpose language (暫用 GPL 代表)更能貼近核心。最顯而易見的例子就是 GUI 的設計,用 WYSIWYG 的方式刻繪介面會比純文字好、BNF  form 比一堆 if, else, loop 湊出來的 FSM 更能表達語言結構、SQL 語法比用 procedural 清晰。此外,好的表達力可以讓維護程式碼時,一目了然,不需太多註解、文件相輔,一行 DSL 程式碼表達的運算可能需要數行 GPL 才能完成,這種精簡效果是很可觀的!
  2. 語法支持:這個部份我最想挑出來說的是 domain-specific error report/checking 和 constraint enforcement。好的語言不僅給予程式設計師自由的空間去發會,還要能適時給予程式設計師提醒與限制。 enforcement 是我進公司後才體驗到的一個 term ,這裡或許反映出一個個人喜好,我不喜歡把錯誤推遲到執行時期發現。不同的應用中常會帶來不同的限制或者說是規範,最糟糕的情況是無法透過 GPL 既有元素去表達的,或是僅能在執行時期偵測、或是以間接、隱晦的方式去表現。
  3. 其他,如可以做到更好的最佳化、程式碼內嵌 domain knowledge、易於測試等。(哈,我用其他來帶過,其實就代表我個人認為前兩項是最重要的好處)

為了避免空口說白話,這邊以 C++ 實做一個矩陣為例子來說明 GPL 可能做不好的地方:

class MyMatrix {
public:
    MyMatrix();
    MyMatrix( int row, int col );

    int operator ()( int row, int col );
    vector<int>& operator []( int row );
    template <typename T>
    MyMatrix& operator =( const T& );

private:
    int     row_;
    int     col_;
    vector< vector<int> >   data_;
};

使用時很簡單,就像下面一樣,精練、不失矩陣的意含(我想是這樣沒錯):

    MyMatrix m1( 10, 10 );

第一步看起來不錯,但是很明顯的當我們想初始化矩陣時,無論我們怎麼努力,卻很難只靠 C++ 本身(先不討論 Preprocessor)做到像下面的效果:

    MyMatrix m2 = { { 0, 0 }, { 0, 0 } };

最多我們只能靠著 operator overloading 做到:

    for ( int i = 0; i < 10; ++i ) {
        for ( int j = 0; j < 10; ++j ) {
            m1[ i ][ j ] = i + j;
        }
    }

或是提供 MyMatrixRow, MyMatrixCol 之類的 Proxy class 去一次設定多個元素;但無論如何,當元素間沒有規則時,一個一個指定矩陣元素的步驟就跑不了,此時若是有個 Matrix 相關的 DSL 輔助,我們可以寫出更精練、具可讀性的程式。下一個我們會面臨的問題可能發生在矩陣運算上,以加法為例,兩個相加的矩陣必須具有同樣的維度,但這個限制(或說規範)在 MyMatrix 是無法直接表達的(請先不要考慮到 C++ template,首先是因為這不是每個語言都有的 feature,其次是之後會有個段論詳細解釋這部份),所以我們可能得在

MyMatrix operator +( const MyMatrix& lhs, const MyMatrix& rhs );

的實做時,去檢查矩陣的維度,當兩者不符合時丟出一個 excption。當然不是說 exception 不好,而是若是能越早發現錯誤會更好,尤其是這種維度的指定與推導是可以靜態完成的,實在沒有必要留待動態時期去偵測。此時若是有個 Matrix 相關的 DSL 可以在撰寫時給予這方面的限制,可以省去不少麻煩、也能提高程式效能。

缺點

說完 DSL 的優點,聽起來是如此好用,讓我也一度著迷,想做些 DSL 出來用,不過現實世界當然不會如此美好!DSL 最大的問題在於:

  1. 草創期
    實做一個 DSL 需要許多人力投入在設計、實做上,先期的 domain/problem 分析、研究更是花費人工。這階段最惱人的應該是穩定度吧?!
  2. 初期
    當語言實做完成後,下一個面臨的問題就是配套與使用者心理。即使語言再簡單,都還是有自己的中心哲學,對於使用者來說:他們是不是快速的學習、語言的 Learning curve 是不是適當呢?甚至使用者可能排斥再學一個新語言?接著實際開發時,是不是相關的 tool chain 都能搭配上,是不是能有好用的 editor、IDE 來輔助開發、吸引開發者目光。而其中最為麻煩的,我想是錯誤資訊(像是 compiler error、failed to code gen …)與 debug 的環境,該如何讓使用者快速 trouble shooting 是很重要的。即使今天我們提供的 DSL 只是做 transformation,把 MyMatrix DSL 轉成 C++ 而已,錯誤資訊的轉換是恨重要的,有用過 lex/yacc 的人可以回憶一下,在寫錯 lex/yacc 的程式時,lex/yacc 的 codegen 程式有時不會知道,它們只能根據預先的條件去轉換成 C 語言,當送給 C compiler 時才會 compile errors,此時使用者該如何從這樣的錯誤對應回 lex/yacc 的錯誤呢?當然最好的方式是,使用者不必以 codegen 後的角度去找尋錯誤,而是直接面對他們寫的 DSL 程式去找錯(像是 lex, yacc 程式碼、MyMatrix),同樣地,debug 也是面臨同樣問題,lex/yacc 雖然在code generation 時會把對應的行號、檔名寫到產生出來的 .c 檔中,但用 C debugger 回頭看到的卻是 FSM 和 LALR 的程式,個人認為使用者是很容易迷路的!
  3. 中期
    維護,像是文件、社群資源等都是需要長時間投入的。
  4. 長期
    想必該是面對改版相容性問題時候了!

DSL 的形式

從 DSL 的實做方式以及它怎麼跟其他語言合作,我們可以大致上分成下面幾種,並且從設計、實做與使用上來討論一下:

  1. Fixed, separate DSLs
    這是最常見的一種形式,DSL 擁有自行一套的 tool chain 甚至執行環境,例如 SQL 。這種形式的 DSL 最大的問題是很難跟其他語言進行良好的整合,可能是語言特性(static vs. dynamic)、執行方式(compiler vs. interpreter)、執行環境的差異、tool chain 等因素造成。即使是可以透過一定的手法把 separate DSLs 轉為 embedded DSLs,但仍不是很方便。而 DSL 實做上呢?從頭到尾打造一個 DSL 是很曠日費時,而且成本極高的一件事,更不要談說相關的開發環境。簡單的說, DSL 和他要合作的語言間太鬆綁了(loose coupling),loose coupling 有時不是件好事。
  2. Embedded DSLs
    另一種方式是我們可以在 GPL 中定義相關的類別、函式去包裝 DSL ,讓開發過程中,DSL 與 GPL 是緊緊地包在一起的,這聽起來比 separate DSLs 好,但是根據實做出的合作方式,卻可能比 separate DSLs 好不到哪裡去,舉個例子來說,很多 database 都會有不同語言的 interface library 供開發者使用,PostgreSQL 有一個 C++ 介面的函式,叫做 libpgxx ,它與 PostgreSQL 連接的方式是:
    int playWithDb() {
        connection Conn( "dbname=test" );
        {
            work Xaction( Conn, "DemoTransaction" );
            result r = Xaction.exec( "DELETE FROM " + Table + " WHERE ID=" + ID );
    
            for ( result::size_type i = 0; i != R.size(); ++i ) {
                Process(R[i]["lastname"]);
            }
        }
    }
    或是看個 C++ 與 TCL 的整合:
    void hello()
    {
         cout << "Hello C++/Tcl!" << endl;
    }
    
    int main()
    {
         interpreter i;
         i.def("hello", hello);
    
         string script = "for {set i 0} {$i != 4} {incr i} { hello }";
    
         i.eval(script);
    }
    不難發現這種倚賴參數、字串來執行 DSL 的方式讓我們失去了機會去做一些事情:
    1. express domain-specific optimization
    2. domain-specific error report:  syntax error 和 debug 是我認為尤其重要的,像是 compile time 的 error/constraint enforcement !
    3. domain-specific syntax
    不過幸運的是,有另外一種方式可以讓我們從 GPL 中衍生 DSL ,那就是 metaprogramming ,雖然不是每種語言都有這種特性,也不是擁有這種特性後就能一切完美。以 C++ 來說,我們可以透過 template metaprogramming 做到 code generation 和 code optimization ,甚至透過 C++ 型別去做部份的 error report/checking。但大部分時候仍是補強而已,很多 domain-specific 的事還是得推延到 run time。這邊想示範一個簡單的 C++ metaprogramming 利用型別實做 constraint enforcement 的概念,那就再次請出 Matrix 來當例子,這次我想針對維度做出 constraint enforcement ,那我們可以把原本的實做改成像下面這樣:
    template <int RowT, int ColT>
    class MyMatrix {
    public:
        friend MyMatrix operator +( const MyMatrix& lhs, const MyMatrix& rhs );
    
        MyMatrix();
    
        int operator ()( int row, int col );
        vector<int>& operator []( int row );
        template <typename T>
        MyMatrix& operator =( const T& );
    
        static const int RowDimension = RowT;
        static const int ColDimension = ColT;
    
    private:
        vector< vector<int> >   data_;
    };
    注意到沒有,我們把維度的概念帶入型別之中,那麼下面的程式碼, 
     
        MyMatrix<10, 10> m1;
        MyMatrix<2, 2>   m2;
    
        cout << m1 + m2 << endl;
    便可以在 compile time 丟出錯誤,下面是 VC 2005 的錯誤訊息:

    error C2679: binary '+' : no operator found which takes a right-hand operand of type 'MyMatrix' (or there is no acceptable conversion)

    但是細心的你注意到了嗎?即使我們把錯誤拉前了,但這樣的錯誤訊息並不是跟矩陣有太大關係,如果能丟出

    incompatible dimensions

    是不是更好呢?在 GPL 中,我們有時是很難做到完整的 domain-specific error report/checking 的 。再者,並不是每種 constraint 都適合以型別的方式表達,一來可能造成 template parameter 數量爆增,這可能增加 compile 時間,二來也暴露太多細節在 client code 中,這會讓程式碼難以擴充維護。
    雖然用了 C++ template 當例子,不過值得提一下, template haskell 似乎也很有趣,許多 functional language 都可以一些語言擴充!
  3. Modularly composable DSLs
    腦子很小的我,只想出前面兩種,這第三種形式是 Generative Programming 一書提出的,它強調的是在語言的基礎建設(infrastructure)完善皆備的情況下,去開發語言的 plug-in ,說來神奇,其實不會, C/C++ 的 preprocessor 或是更古老的 Smalltalk 和 CLOS 都是這種概念的雛型。只是它們未能在 domain-specific 的需求上達到更好的精細度。或許看到這裡,你會有跟我一樣的一個疑問:那 Modularly composable DSLs 與 Embedded DSLs 有什麼不同呢?聽起來都是建構在既有語言之上,其實這中間有個隱微的差異,可以細細體驗一下:

    Embedded DSLs 的根基是現有語言的 syntax,而 Modularly composable DSLs 則是做 syntax extensions。

    這句話代表的是,Embedded DSLs 的使用是類似於使用 GPL 的 library 或是 framework 的,其 syntax/semantic 跟原本 GPL 是一樣的。但 Modularly composable DSLs 是構造在 GPL infrastructure 之上,除了既有的 syntax/semantic 外,我們還可以增加 domain-specific 的 syntax/semantic ,甚至是刪去、捨棄既有的。若是想更細部條列兩者的差異,或許可以參考 Generative Programming 所條列的,Modularly composable DSLs 可以比 Embedded DSLs 做到:
    1. Syntax extensions
    2. Semantic extensions or modifications of language constructs
    3. Domain-specific optimizations
    4. Domain-specific type systems
    5. Domain-specific error checking and reporting
    再回到 language infrastructure 上, 我們有什麼辦法可以操作 language infrastructure 呢?有幾種可能性:
    1. Preprocessor
    2. Metaprogramming
    3. Modularly Extendible Compilers and Modularly Extendible Programming Environments
    前兩者在許多語言上已經可以做到,正如前面提到的 C/C++, Smalltalk, Haskell 和 CLOS 都行,只是做的不夠好。因為這不只是語言本身支援的問題,還牽扯到 tool chain 與環境。至於第三種,則可以參考 Charles Simonyi (是的,就是微軟的那位大師,匈牙利命名法、WYSIWYG document 發明者、 Word 之父)的研究 – Intentional Programming 。大師在 Microsoft Research 以及後來開設的 Intentional Software 公司都可以找到這方面的發表與研究成果,研究開始得很早,約莫在上個世紀的 90 年代初,未來有時間,希望能玩玩看再來跟大家分享一下!

結語

這篇文章真是超長,內容又很空洞,可是我打了兩天,大概是我在低潮吧…不過還是想跟大家分享一下幾點:

  1. Compiler is your friend!
  2. Constraint enforcement is your friend!
  3. Compile-time error report/checking is your friend!
  4. The last but most important point, C++ is your friend!

10 則留言:

87showmin 提到...

細節的部份我有空會拜讀完畢,你害我又要晚睡了 ~"~,我對這東西也有些想法,不過就請移駕到我 blog 參觀囉。

Keiko 提到...

哈,寫得沒有很好,只是單純覺得是時間該寫點東西了,不必拜讀、跪讀、叩讀、齋戒沐浴、焚香祭祀向北朝拜、再讀之類的!

不過今天能回學校見到大家,實在很高興,希望以後有機會可以多回去!

Zhong-Ho Chen 提到...

Recommendation:
Resubmit after Major Revision for Review

Comments to the Author:
這篇文章介紹DSL以及可能的實作方法, 並主張C++和Compiler是好朋友, 我認為作者應該提出更多證據, 例如將提出的方法應用到SLIM或小七妹

Keiko 提到...

哈哈, aaa 真是幽默,不愧是為了 paper 閉過關的人!

不過你說得沒錯,應該要有更多的證據,或許哪天來把 C++09 的一些新 feature 拿來介紹一下,新的 C++ 期望能從 compiler 手中拿到更多的資訊,做到更好、更簡潔的表示!

fr3@K 提到...

> MyMatrix m2 = { { 0, 0 }, { 0, 0 } };
C++98 with Boost.Assign or
C++0x 的 initializer lists

Keiko 提到...

哈,謝謝 fr3@k 的回應,看來下次不能再拿 matrix 當例子了!

fr3@K 提到...

還沒完... 請 移駕.

匿名 提到...

原諒我的淺薄, 我第一次看到DSL時, 腦子裡浮現的是digital subscriber line. XD.

SCREAM有在想這件事嗎? 當初的javaol也許是吧! 看來這不是一個遙不可及的夢.有沒有一個對musician非常friendly的language呢? 我第一次到ccrma時用過common lisp後, 覺得應該要有更好的才對, 雖然我的音樂家朋友能有這東西用就似乎很高興了.

今天有一個想法, 有沒有一個簡單到不行的language可以讓我設計我的新家, 我的唱臂, 我的音樂, ..., 我已經被成大所為我帶來的一切鎖到我幾乎失去自由, 我似乎變成了只剩下腦子有想法, 手上卻沒方法的糟老頭了. 沒有smallnew, buffett, Aaa, ..., 我甚麼也沒法子做 . 我懷疑我是不是該考慮退休了嗎?

當然了, 我家那兩位寶貝也是原因之一. 我不應該只怪成大的. 不行, 我還要尸位素餐下去, 要騙得到Diane跟Joanne所需的學費與嫁妝才行.

有沒有那個programming language可以讓我跳過一些mumbo jumbo, 讓我的想法可以virtualized, visualized, and finally, realized.

好吧! 我在scream blog裡說過. 在HW的世界裡, 總是有人在做一些事情幫助你programming, testing, and debugging. For example, Design For Test (DFT), auto test bench, fault detection, and so on. 對HW來說, compile過不是問題, compile過後, functionality不對, performance不好, 出現unexpected faults,...,等才是問題的開端. 偏偏這又可能不是你造成的, 誰知道你的lib是甚麼特性. 這個design flow不只是 hdl是domain specific, 所有的tools都是. high level design 發展至今, 問題還沒解完, 所以做一顆好的SoC還是那麼困難.


scream lab好像最近都想試著有限度地解一些些類似的問題, 包括前面講的javaol, 還有slim, openesl, dataflow.

臨開口的表而涕泣, 不知所云.

抱歉, 今天身心都累了. 尤其是當別人把我们用在大學生或高中生身上的壞技倆回用在自己身上時. 真是報應ㄚ.

四月十日左右, 去台北跟學長學姐聚聚. 請幫我聯絡.

匿名 提到...

Just in case keiko說我孔明的出師表沒讀懂.

我是說, 看完keiko的文章, 自己因為笨得不知keiko在說什麼而悲泣.

最近有個學弟想要把javaol弄一下, 看看是不是也可以像chuck一樣, keiko可以表示一下意見嗎? 因為是orchestra language, 所以該跟domain specific有關連.

Keiko 提到...

哈哈,老師你言重了,套句曹先生的話:這是滿紙荒唐言。因為當時心情很悶,所以就寫了些東西。

老師當然還要繼續感召學弟、學妹好幾年、好幾年。學術領域之外,我想老師更厲害的是跟大家的聊天,這是不會隨著時間用來用消退的功力。

老師有提到:最近有個學弟想要做 orchestra languag 。我想我能幫的忙不大,因為這是 orchestral domain ,我從小音樂課都是靠乖乖牌形象才過的 Orz 至於 DSL 相關的東西,我想是可以跟學弟討論些什麼的,不過我也不是專家,只能盡棉薄之力!

Windows + Visual Studio + VSCode + CMake 的疑難雜症

Environment Windows 10 Visual Studio 2019 CMake 3.27.7 VSCode VSCode CMake Tools 1. CMAKE_BUILD_TYPE 是空的 參考一下 這篇 的處理。 大致上因為 Visual...