IT技术博客大学习 共学习 共进步
全部 移动开发 后端 数据库 AI 算法 安全 DevOps 前端 设计 开发者

从Java和JavaScript来学习Haskell和Groovy(DSL)

四火的唠叨 2016-02-21 22:47:37 累计浏览 2,262 次
本机暂存

   从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接口呢?

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

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

   这个@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));

   动态语言和丰富语法糖的关系,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

   上面定义了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..]

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

   下一篇,也预计是最后一篇,我想着重介绍一下整体的角度来看时,编程范型的部分。

   文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接《四火的唠叨》

分享到:

同分类推荐文章

  1. 科技爱好者周刊(第 401 期):如何赚到10亿美元 (2026-06-26 08:05:38)
  2. 如何做决策 - 从 Go 的一个 issue 说起 (2026-06-26 08:00:00)
  3. Seven Player:Windows上播放115网盘视频的增强工具 (2026-06-09 00:06:47)

查看更多 开发者 文章 →

建议继续学习

  1. SmartSprites - 命令行形式的CSS Sprites生成器 (累计阅读 123,900)
  2. JQuery实现Excel表格呈现 (累计阅读 48,352)
  3. Java开发岗位面试题归类汇总 (累计阅读 22,161)
  4. android 开发入门 (累计阅读 19,533)
  5. 深入理解Javascript之执行上下文(Execution Context) (累计阅读 18,411)
  6. 从输入 URL 到页面加载完成的过程中都发生了什么事情? (累计阅读 15,938)
  7. 图片动态局部毛玻璃模糊效果的实现 (累计阅读 14,849)
  8. 天朝第二代身份证号码的验证机制 (累计阅读 14,764)
  9. HTML 5 的data-* 自定义属性 (累计阅读 14,353)
  10. 分享一个JQUERY颜色选择插件 (累计阅读 14,225)