天天看點

關于DSL的一些思考

前言

随着近幾年新語言(例如,rust、kotlin、dart等10後的語言)的出現,經常有同僚問起“最近有個xx語言挺火,你怎麼看”這類問題。這種情況下,一般都不太想回答(根本不知道從哪說起…)。平時身邊的同僚中是某個語言的使用者或者說是依賴/利用某個語言來完成工作的居多,把某個語言用得如手腳般靈活的極少,把自己擅長的語言控制得如手術刀那樣精細的更少,每次自己寫的程式出了問題5分鐘都查不出原因,我會把這樣的情況歸結為–程式失控了。沒有辦法,工作中很多時候都是趕鴨子上架,兩天學一個語言,一天學架構,然後直接做項目也不是什麼罕見的事了。每次看到這個情況,我都會感覺,那個開發者被語言綁架了,他模仿書上和網上的代碼來編碼,而不是結合應用場景、自己的思維方式和喜好來設計代碼。而本文接下來要講的DSL可以幫助開發者成為程式設計語言的主人(一定程度上)。

DSL是什麼

DSL是領域特定語言(domain-specific language)的縮寫,先看兩個定義:

  1. 領域特定語言指的是專注于某個應用程式領域的計算機語言。(來自百度百科)
  1. 所謂DSL,是指為特定領域所專門設計的詞彙和文法,簡化程式設計過程,提高生産效率的技術,同時也讓非程式設計領域專家直接描述邏輯成為可能。(來自松本行弘《代碼的未來》)

或許看定義不是非常形象生動,那麼,來看下面幾個例子:

  1. HTML

一個标題為title切内容為content的網頁:

<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>title</title>
    </head>
    <body>
        <div>content</div>
    </body>
</html>
           
  1. SQL

從表table1中查找處id為1的項:

  1. Common Lisp的Loop宏

i從10下降到1,把i列印并收集起來:

(loop for i from 10 downto 1 do (print i) collect i)
           

看到這裡,大家應該明白為什麼叫領域特定語言了:Html特定于web頁面展示領域,SQL特定于資料庫查詢領域,Loop宏則特定于循環疊代的場景。(有點像“子語言”或“内嵌語言”的感覺)

DSL的分類

DSL根據實作方式的不同,可以分為内部DSL和外部DSL。

外部DSL

定義:使用專用的語言引擎來實作的DSL,例如:SQL、HTML、Awk。

優點:

1. 獨立于程式開發所使用的語言;

2. 根據目的設計文法,不被現有語言的文法所影響;

缺點:

1. 學習成本較高(畢竟要學習一個全新的語言)

内部DSL

定義:在現有的語言中,利用語言的特性實作的DSL,例如:多種Lisp方言,開發者自己使用Scala、Ruby實作的DSL。

優點:

1. 學習成本較低,無需學習一本新語言;

2. 可以使用宿主語言的全部特性;

3. 無需實作全部的詞法、文法解析,實作成本低;

缺點:

1. 可以實作的文法和語義受宿主語言影響;

2. DSL依賴于程式開發所使用的語言;

為什麼使用DSL

“計算機的語言會影響你怎樣思考問題,影響你怎樣看待資訊傳播。”

這是《程式員修煉之道》中的看法,其中提到一點就是靠近問題域程式設計,我比較認同,畢竟語言最主要的作用是描述資訊。在開發功能時,要把這個功能用程式設計語言簡潔、直覺地表述出來,直接使用這個功能領域的文法和名詞是最合适的。例如,下面這個網絡請求的示例:

  • 功能描述:發送一個get請求,搜尋指定關鍵字的指定頁的内容
  • 接口描述:

    請求URL:

    • http://www.test.com/api/search

    請求方式:
    • GET
    參數:
    參數名 必選 類型 說明
    keyword string 關鍵字
    page int 頁碼

假設關鍵字為 test,頁碼為1,那麼大緻資訊總結如下:(其中一種設計,其他形式可自己思考)

http /www.test.com/api/search
    keyword : test
    page : 1
get
res => print res
           

使用Lisp實作的内部DSL效果如下:

(http /www.test.com/api/search
    args = ((keyword test)
            (page 1))
    get
    (res => (print res)))
           

Java雖然沒有什麼支援DSL的特性,不過可以使用流式接口(Fluent Interface)來實作類似的效果,例如:

NetTool.http("/www.test.com/api/search")
        .arg("keyword", "test")
        .arg("page","1")
        .get() //這裡傳回RxJava的Observable
        .map(SearchResult::fromJson)
        .subscribe((SearchResult result) -> {
            ///TODO 處理結果
        })
           

以上的實作方式,和普通的代碼比較起來,簡潔很多吧,邏輯清晰,沒有缺少資訊,也沒有多出什麼不必要的字元。

DSL的缺點

最後還是得提一下DSL的缺點,畢竟一個東西沒有缺點就太假了,在使用技術的過程中揚長避短才是明智之舉

上面說到了内部DSL和外部DSL之間對比的優缺點,下面是和沒有采用DSL的情況對比的缺點:

  1. 學習成本高。不管是内部DSL還是外部DSL,學習成本都比無DSL的情況要高。而且,很多時候,内部DSL的開發者并不會準備詳盡的文檔和教程,導緻學習難度大;
  2. 如果用外部DSL,則面臨多語言混合程式設計的共有問題,文法一緻性差,試想一下:一張圖檔,上半張是西瓜,下半張是蘋果的樣子。例如,一個html檔案裡同時又有js代碼(雖然可以通過訓練來克服,但是我就是不喜歡);
  3. 如果使用内部DSL,那麼可能面臨設計的DSL和宿主語言之間不同程式設計思想的沖突,例如,平常使用Java進行面向對象程式設計,而用流式接口實作近似DSL時,會使用一些函數式程式設計的思想,但是并不是所有人都熟悉函數式程式設計(雖說OOP和FP并不對立,不過要在實際項目中合理的融合這兩種風格也不是簡單的事 >_<);

結語

DSL是把雙刃劍,這是不争的事實,或者說,所有的技術、語言、架構在不正确使用時都是有害的。在适當的時機、适當的場合設計适當的DSL來解決适當的問題,很可能寫出“明顯沒有bug的代碼”,避免“沒有明顯bug的代碼”。(至于這個“适當”就隻能由開發者自行判斷了。。。)