❲DSL触类旁通❳从Java和JavaScript来学习Haskell和Groovy

Posted VanillaOpenResty开发

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了❲DSL触类旁通❳从Java和JavaScript来学习Haskell和Groovy相关的知识,希望对你有一定的参考价值。

来源:四火的唠叨
原文地址:http://www.raychase.net/3110

当我们面对各种各样的特定需求的时候,一个通用的语言往往不能高效地提供解决问题的路径,相应的DSL并不是并非要解决所有的问题,但是它只关注于某一个小领域,以便解决那一个小领域的问题就好了。比如html,只用于网页渲染,出了这个圈子它什么都不做,但是用来表达网页的内容却很擅长,有很多内置的标签来表达有预定义含义的内容;再比如SQL,只能写数据库相关的操作语句,但是很适合用来描述要查询什么样的一个数据集合,要对数据集合中的元素做什么样的操作。


首先来理解DSL

DSL(Domain Specific Language:https://en.wikipedia.org/wiki/Domain-specific_language)指的是一定应用领域内的计算机语言,它可以不强大,它可以只能在一定的领域内生效(和GPL相比,GPL是General Purpose Language),表达仅限于该领域,但是它对于特定领域简洁、清晰,包含针对特定领域的优化。


先来看Java

用Java写DSL是可能的,但是写高效和简洁的DSL是困难的。原因在于它的语法限制,必须严谨的括号组合,不支持脚本方式执行代码等等。

首先讲讲链式调用。这也不是Java特有的东西,只不过Java的限制太多,能帮助DSL的特性很少,第一个能想到的就是它而已。比如这样的代码,组织起html文本来显得有层次、有条理:

❲DSL触类旁通❳从Java和JavaScript来学习Haskell和Groovy

document

.html()

.body()

.p()

.text("context 1")

.end()

.p()

.text("context 2")

.end()

.end()

.end()

.creat();

链式调用还有一个令人愉快的特性是泛型传递,我在这篇文章中介绍过(http://www.raychase.net/2446),可以约束写DSL的人使用正确的类型。

其次是嵌套函数,这也不是Java特有的东西,它和链式调用组成了DSL最基本的实现形式:

❲DSL触类旁通❳从Java和JavaScript来学习Haskell和Groovy

new Map(

city("Beijing", x1, y1),

city("Shanghai", x2, y2),

city("Guangzhou", x3, y3)

);

值得一提的是Java的闭包,可以说闭包是融合了管道操作和集合操作美感的,谈DSL不能不谈闭包。但是,直到2014年4月JSR-335才正式final release,不能不说这个来得有点晚。有了闭包,有了Lambda表达式(其实本质就是匿名函数),也就有了使用函数式编程方式在Java中思考的可能。

考虑一下排序的经典例子,可以自定义Comparator<T>接口的实现,从而对特定对象列表进行排序。对于这样的类T:

❲DSL触类旁通❳从Java和JavaScript来学习Haskell和Groovy

public class T {

public Integer val;

}

可以使用匿名的Comparable实现类来简化代码:

❲DSL触类旁通❳从Java和JavaScript来学习Haskell和Groovy

Collections.sort(list, new Comparator<T>() {

@Override

public int compare(T o1, T o2) {

return o1.val.compareTo(o2.val);

}

});

但是如果使用JDK8的Lambda表达式,代码就简化为:

❲DSL触类旁通❳从Java和JavaScript来学习Haskell和Groovy

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

更加直观,简洁。

那么为什么 (x,y) -> y-x 这样的Lambda表达式可以被识别为实现了Comparator接口呢?

原来这个接口的定义利用了这样的语法糖:

❲DSL触类旁通❳从Java和JavaScript来学习Haskell和Groovy

@FunctionalInterface

public interface Comparator<T> {

...

}

这个@FunctionalInterface的注解,是可以用来修饰“函数接口”的,函数接口要求整个接口中只有一个非java.lang.Object中定义过的抽象的方法(就是没有具体实现的方法,且方法签名没有在java.lang.Object类中出现过,因为所有类都会实现自java.lang.Object的,那么该类中已定义的方法可以认为已经有默认实现,接口中再出现就不是抽象方法了)。

好,有了这一点知识以后还是回头看这个Comparator接口的定义,有这样两个抽象方法:

❲DSL触类旁通❳从Java和JavaScript来学习Haskell和Groovy

int compare(T o1, T o2);

boolean equals(Object obj);

那么按照刚才的说法,其中的equals方法是在java.lang.Object中出现过的,不算,在考察函数接口的合法性时,其实只有一个compare这一个抽象方法。

顺便加一句吐槽。该接口还有几个方法的default实现,“接口的默认方法”,为了在增加行为的情况下,考虑向下兼容,总不能把Comparator把接口改成抽象类吧,于是搞了这样一个语法糖,但是它是如此地毁曾经建立的三观,接口已经可以不再是纯粹的抽象了。


接着来看javascript的DSL

其实就DSL的实现而言,Java和JavaScript来实现并没有非常多的区别,最大的区别可能是,JavaScript中,function可以成为一等公民,因此能够写更加灵活的形式:

❲DSL触类旁通❳从Java和JavaScript来学习Haskell和Groovy

new Wrapper([1, 2, 5, 3, 4])

.filter(filterFunc)

.map(mapFunc)

.sort()

.zipWith([7, 8, 9, 10, 11]);

再给一个高阶函数(Curry化)的例子:

❲DSL触类旁通❳从Java和JavaScript来学习Haskell和Groovy

var logic = new Logic()

.whenTrue(exp1)

.whenFalse(exp2);


console.log(logic.test(3>2));

动态语言和丰富语法糖的关系,Groovy是非常适合用来写DSL的。一方面是因为语法糖的关系,万物皆对象,省掉不少累赘的括号,代码看起来比较自然,接近自然语言;另一方面是有不少语言特性,比如MethodMissing,帮助写出简洁的DSL。下面分别说明,例子大多来自这个官网页面(http://docs.groovy-lang.org/docs/latest/html/documentation/core-domain-specific-languages.html)

❲DSL触类旁通❳从Java和JavaScript来学习Haskell和Groovy

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

take 2.pills of chloroquinine after 6.hours

看到上面这个,因为简简单单的语法糖,就使得代码如此接近自然语言,是否有很心旷神怡的感觉?

这个是个更复杂一些的例子:

❲DSL触类旁通❳从Java和JavaScript来学习Haskell和Groovy

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

上面定义了show和square_root的闭包,然后在please方法中,调用返回了一个对象,可以继续调用the方法,其结果可以继续调用of方法。action是please方法的闭包参数,square_root是the方法的闭包参数。挺有趣的,好好品味一下。

再有这个我曾经举过的例子,生成HTML树,利用的就是MethodMissing(执行某一个方法的时候,如果该方法不存在,就可以跳到特定的统一的某个方法上面去),这样避免了写一大堆无聊方法的问题:

❲DSL触类旁通❳从Java和JavaScript来学习Haskell和Groovy

def page = new MarkupBuilder()

page.html {

head { title 'Hello' }

body {

a ( href:'http://...' ) { 'this is a link' }

}

}

当然了,Groovy已经内置了一大堆常用的builder,比如这个JsonBuilder:

❲DSL触类旁通❳从Java和JavaScript来学习Haskell和Groovy

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,这个,我在前面一篇《元编程》(http://www.raychase.net/3062)中已经介绍过了。

最后来说Haskell

作为语言特性的一部分,利用

(1)模式匹配的守护语句

(2)List Comprehension带来的条件分类

免去了if-else的累赘,对于逻辑的表达,可以极其简约。

关于上面(1)模式匹配的部分,《元编程》中已经有过介绍,下面给一个(2)List Comprehension的经典例子,快排:

❲DSL触类旁通❳从Java和JavaScript来学习Haskell和Groovy

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中,所有的函数都可以理解为,每次调用最多都只接受一个参数,如果有多个参数怎么办?把它化简为多次调用的嵌套,而非最后一次调用,都可视为高阶函数(返回函数的函数)。比如:

❲DSL触类旁通❳从Java和JavaScript来学习Haskell和Groovy

Prelude> :t max

max :: Ord a => a -> a -> a

上面描述的调用本质决定了为什么它的结构是a->a->a:接受一个类型a的参数,再接受一个类型a的参数,最终返回的类型和入参相同。

也就是说,这两者是等价的:

❲DSL触类旁通❳从Java和JavaScript来学习Haskell和Groovy

max 1 2

(max 1) 2

继续谈论和DSL相关的语言特性,尾递归和惰性求值。

对于尾递归不了解的朋友可以先参考维基百科上的解释(https://zh.wikipedia.org/wiki/%E5%B0%BE%E8%B0%83%E7%94%A8)。如果递归函数的递归调用自己只发生在最后一步,并且程序可以把这一步的入栈操作给优化掉,也就是最终可以使用常量栈空间的,那么就可以说这个程序/语言是支持尾递归的。

它有什么好处?因为可以使用常量栈空间了,这就意味着再也没有递归深度的限制了。

不过话说回来,Haskell是必须支持尾递归的。因为对于常规语言,如果面临递归工作栈过深的问题,可以优化为循环解决问题;但是在Haskell中,是没有循环语法的,这就意味着必须用尾递归来解决这个本来得用循环才能解决的问题。

给一个例子,利用尾递归,我们自己来实现list求长度的函数:

❲DSL触类旁通❳从Java和JavaScript来学习Haskell和Groovy

len :: (Num b) => [a] -> b

len [] = 0

len (_:xs) = 1 + len xs

然后是惰性求值,直到最后一步,非要求值不可前,不做求值的操作。听起来简单,但是只有Haskell是真正支持惰性求值的,其他的语言最多是在很局限的范围内,基于优化语言运行性能的目的,运行时部分采用惰性求值而已。

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

❲DSL触类旁通❳从Java和JavaScript来学习Haskell和Groovy

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

这是对于正整数序列(无限集合)中的每个元素,平方以后再判断奇偶性,只取奇数的结果,最后再判断是否小于10,最后再把满足条件的这些结果全部加起来。

当然,利用语法糖,可以把上面讨厌的嵌套给拉平,从而去掉那些恼人的括号:

❲DSL触类旁通❳从Java和JavaScript来学习Haskell和Groovy

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

两者功能上没有任何区别。


Vanilla社区发起⦗晨读计划⦘,每天坚持积累一点,今天的努力至少让我们比昨天更进一步。

⦗晨读计划⦘ 期待你的加入... ...

Vanilla:基于OpenResty的高性能Web应用开发框架
❲DSL触类旁通❳从Java和JavaScript来学习Haskell和Groovy


晨读计划

2016/01/06


以上是关于❲DSL触类旁通❳从Java和JavaScript来学习Haskell和Groovy的主要内容,如果未能解决你的问题,请参考以下文章

编程语言总结

运行自定义构建的 Kafka 流 DSL 应用程序返回 java.lang.ClassNotFoundException

怎么成为一名程序员,要从哪里开始学起,先学啥在学啥

如何使用 spring 集成 dsl 从 JMS 队列中解组 XML

Java NIO 选择器(Selector)的内部实现(poll epoll)

学习JAVA