從Java和JavaScript來學習Haskell和Groovy(DSL)

cg4f 10年前發布 | 19K 次閱讀 Java JavaScript開發

從Java和JavaScript來學習Haskell和Groovy(DSL)
這是《從Java和JavaScript來學習Haskell和Groovy》系列的第四篇。

首先來理解DSL。

DSL( Domain Specific Language )指的是一定應用領域內的計算機語言,它可以不強大,它可以只能在一定的領域內生效(和GPL相比,GPL是General Purpose Language),表達僅限于該領域,但是它對于特定領域簡潔、清晰,包含針對特定領域的優化。

當我們面對各種各樣的特定需求的時候,一個通用的語言往往不能高效地提供解決問題的路徑,相應的DSL并不是并非要解決所有的問題,但是它只關注于某一個小領域,以便解決那一個小領域的問題就好了。比如HTML,只用于網頁渲染,出了這個圈子它什么都不做,但是用來表達網頁的內容卻很擅長,有很多內置的標簽來表達有預定義含義的內容;再比如SQL,只能寫數據庫相關的操作語句,但是很適合用來描述要查詢什么樣的一個數據集合,要對數據集合中的元素做什么樣的操作。

先來看Java。用Java寫DSL是可能的,但是寫高效和簡潔的DSL是困難的。原因在于它的語法限制,必須嚴謹的括號組合,不支持腳本方式執行代碼等等。

首先講講鏈式調用。這也不是Java特有的東西,只不過Java的限制太多,能幫助DSL的特性很少,第一個能想到的就是它而已。比如這樣的代碼,組織起html文本來顯得有層次、有條理:

document
  .html()
    .body()
      .p()
        .text("context 1")
      .end()
      .p()
        .text("context 2")
      .end()
    .end()
  .end()
.creat();

鏈式調用還有一個令人愉快的特性是泛型傳遞,我在這篇文章中介紹過,可以約束寫DSL的人使用正確的類型。

其次是嵌套函數,這也不是Java特有的東西,它和鏈式調用組成了DSL最基本的實現形式:

new Map(
  city("Beijing", x1, y1),
  city("Shanghai", x2, y2),
  city("Guangzhou", x3, y3)
);

值得一提的是Java的閉包,可以說閉包是融合了管道操作和集合操作美感的,談DSL不能不談閉包。但是,直到014年4月 JSR-335 才正式final release,不能不說這個來得有點晚。有了閉包,有了Lambda表達式(其實本質就是匿名函數),也就有了使用函數式編程方式在Java中思考的可能。

考慮一下排序的經典例子,可以自定義Comparator<T>接口的實現,從而對特定對象列表進行排序。對于這樣的類T:

public class T {
    public Integer val;
}

可以使用匿名的Comparable實現類來簡化代碼:

Collections.sort(list, new Comparator<T>() {
    @Override
    public int compare(T o1, T o2) {
        return o1.val.compareTo(o2.val);
    }
});

但是如果使用JDK8的Lambda表達式,代碼就簡化為:

Collections.sort(list, (x, y) -> y - x);

更加直觀,簡潔。

那么為什么 (x,y) -> y-x 這樣的Lambda表達式可以被識別為實現了 Comparator接口 呢?

原來這個接口的定義利用了這樣的語法糖:

<pre>@FunctionalInterface public interface Comparator<T> { ... }</pre>

這個 @FunctionalInterface的注解 ,是可以用來修飾“函數接口”的,函數接口要求整個接口中只有一個非java.lang.Object中定義過的抽象的方法(就是沒有具體實現的方法,且方法簽名沒有在java.lang.Object類中出現過,因為所有類都會實現自java.lang.Object的,那么該類中已定義的方法可以認為已經有默認實現,接口中再出現就不是抽象方法了)。

好,有了這一點知識以后還是回頭看這個 Comparator接口 的定義,有這樣兩個抽象方法:

int compare(T o1, T o2);
boolean equals(Object obj);

那么按照剛才的說法,其中的equals方法是在java.lang.Object中出現過的,不算,在考察函數接口的合法性時,其實只有一個compare這一個抽象方法。

順便加一句吐槽。該接口還有幾個方法的default實現,“接口的默認方法”,為了在增加行為的情況下,考慮向下兼容,總不能把Comparator把接口改成抽象類吧,于是搞了這樣一個語法糖,但是它是如此地毀曾經建立的三觀,接口已經可以不再是純粹的抽象了。

接著來看JavaScript的DSL。其實就DSL的實現而言,Java和JavaScript來實現并沒有非常多的區別,最大的區別可能是,JavaScript中,function可以成為一等公民,因此能夠寫更加靈活的形式:

new Wrapper([1, 2, 5, 3, 4])
  .filter(filterFunc)
  .map(mapFunc)
  .sort()
  .zipWith([7, 8, 9, 10, 11]);

再給一個高階函數(Curry化)的例子:

var logic = new Logic()
  .whenTrue(exp1)
  .whenFalse(exp2);

console.log(logic.test(3>2));</pre>

動態語言和豐富語法糖的關系,Groovy是非常適合用來寫DSL的。一方面是因為語法糖的關系,萬物皆對象,省掉不少累贅的括號,代碼看起來比較自然,接近自然語言;另一方面是有不少語言特性,比如MethodMissing,幫助寫出簡潔的DSL。下面分別說明,例子大多來自這個 官網頁面

// equivalent to: take(2.pills).of(chloroquinine).after(6.hours)
take 2.pills of chloroquinine after 6.hours

看到上面這個,因為簡簡單單的語法糖,就使得代碼如此接近自然語言,是否有很心曠神怡的感覺?

這個是個更復雜一些的例子:

show = { println it }
square_root = { Math.sqrt(it) }

def please(action) { [the: { what -> [of: { n -> action(what(n)) }] }] }

// equivalent to: please(show).the(square_root).of(100) please show the square_root of 100 // ==> 10.0</pre>

上面定義了show和square_root的閉包,然后在please方法中,調用返回了一個對象,可以繼續調用the方法,其結果可以繼續調用of方法。action是please方法的閉包參數,square_root是the方法的閉包參數。挺有趣的,好好品味一下。

再有這個我曾經舉過的例子,生成HTML樹,利用的就是MethodMissing(執行某一個方法的時候,如果該方法不存在,就可以跳到特定的統一的某個方法上面去),這樣避免了寫一大堆無聊方法的問題:

def page = new MarkupBuilder()
page.html {
  head { title 'Hello' }
  body {
    a ( href:'http://...' ) { 'this is a link' }
  }
}

當然了,Groovy已經內置了 一大堆常用的builder ,比如這個JsonBuilder:

JsonBuilder builder = new JsonBuilder()
builder.records {
  car {
      name 'HSV Maloo'
      make 'Holden'
      year 2006
      country 'Australia'
      record {
        type 'speed'
        description 'production pickup truck with speed of 271kph'
      }
  }
}
String json = JsonOutput.prettyPrint(builder.toString())

利用元編程的一些特性,也可以讓一些本來沒有的方法和功能,出現在特定的對象上面,從而支持DSL。比如Categories,這個,我在前面一篇《元編程》中已經介紹過了。

最后來說Haskell。

作為語言特性的一部分,利用(1)模式匹配的守護語句和(2)List Comprehension帶來的條件分類,免去了if-else的累贅,對于邏輯的表達,可以極其簡約。

關于上面(1)模式匹配的部分,《元編程》中已經有過介紹,下面給一個(2)List Comprehension的經典例子,快排:

quicksort :: (Ord a) => [a] -> [a]
quicksort [] = []
quicksort (x:xs) =
  let smallerSorted = quicksort [a | a <- xs, a <= x]
       biggerSorted = quicksort [a | a <- xs, a > x]
  in smallerSorted ++ [x] ++ biggerSorted

上面這個快排算法,清晰,而且簡潔。相比以前用Java寫的快排,用Haskell寫真是太酷了。

前文已經介紹過了高階函數的使用,但是在Haskell中,所有的函數都可以理解為,每次調用最多都只接受一個參數,如果有多個參數怎么辦?把它化簡為多次調用的嵌套,而非最后一次調用,都可視為高階函數(返回函數的函數)。比如:

Prelude> :t max
max :: Ord a => a -> a -> a

上面描述的調用本質決定了為什么它的結構是a->a->a:接受一個類型a的參數,再接受一個類型a的參數,最終返回的類型和入參相同。

也就是說,這兩者是等價的:

max 1 2
(max 1) 2

繼續談論和DSL相關的語言特性,尾遞歸和惰性求值。

對于尾遞歸不了解的朋友可以先參考 維基百科上的解釋 。如果遞歸函數的遞歸調用自己只發生在最后一步,并且程序可以把這一步的入棧操作給優化掉,也就是最終可以使用常量棧空間的,那么就可以說這個程序/語言是支持尾遞歸的。

它有什么好處?因為可以使用常量棧空間了,這就意味著再也沒有遞歸深度的限制了。

不過話說回來,Haskell是必須支持尾遞歸的。因為對于常規語言,如果面臨遞歸工作棧過深的問題,可以優化為循環解決問題;但是在Haskell中,是沒有循環語法的,這就意味著必須用尾遞歸來解決這個本來得用循環才能解決的問題。

給一個例子,利用尾遞歸,我們自己來實現list求長度的函數:

len :: (Num b) => [a] -> b
len [] = 0
len (_:xs) = 1 + len xs

然后是惰性求值,直到最后一步,非要求值不可前,不做求值的操作。聽起來簡單,但是只有Haskell是真正支持惰性求值的,其他的語言最多是在很局限的范圍內,基于優化語言運行性能的目的,運行時部分采用惰性求值而已。

有了惰性求值,可以寫出一些和無限集合之間的互操作,比如:

sum (takeWhile (<10) (filter odd (map (^2) [1..])))

這是對于正整數序列(無限集合)中的每個元素,平方以后再判斷奇偶性,只取奇數的結果,最后再判斷是否小于10,最后再把滿足條件的這些結果全部加起來。

當然,利用語法糖,可以把上面討厭的嵌套給拉平,從而去掉那些惱人的括號:

sum . takeWhile (<10) . filter odd . map (^2) $ [1..]

兩者功能上沒有任何區別。

下一篇,也預計是最后一篇,我想著重介紹一下整體的角度來看時,編程范型的部分。

原文 http://www.raychase.net/3110

 本文由用戶 cg4f 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!