Skip to content

四火的唠叨

一个纯正程序员的啰嗦

Menu
  • 所有文章
  • About Me
  • 关于四火
  • 旅行映像
  • 独立游戏
  • 资源链接
Menu

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

Posted on 07/13/201510/08/2024 by 四火

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..]

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

 

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

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

×Scan to share with WeChat

你可能也喜欢看:

  1. Groovy on Grails 交流活动
  2. 从 Java 和 JavaScript 来学习 Haskell 和 Groovy(类型系统)
  3. Javascript Memoizer
  4. java.util.concurrent 并发包诸类概览
  5. JavaScript 3D 图表

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

订阅·联系

四火,啰嗦的程序员一枚,现居西雅图

Amazon Google Groovy Hadoop Haskell Java JavaScript LeetCode Oracle Python Spark 互联网 前端 华为 历史 同步 团队 图解笔记 基础设施 工作 工作流 工具 工程师 应用系统 异步 微博 思考 技术 数据库 曼联 测试 生活 程序员 管理 系统设计 缓存 编码 编程范型 英语 西雅图 设计 评审 问题 面试 项目

分类

  • Algorithm and Data Structure (30)
  • Concurrency and Asynchronization (6)
  • System Architecture and Design (43)
  • Distributed System (18)
  • Tools Frameworks and Libs (13)
  • Storage and Data Access (8)
  • Front-end Development (33)
  • Programming Languages and Paradigms (55)
  • Testing and Quality Assurance (4)
  • Network and Communication (6)
  • Authentication and Authorization (6)
  • Automation and Operation Excellence (13)
  • Big Data and Machine Learning (5)
  • Product Design (7)
  • Hiring and Interviews (14)
  • Project and Team Management (14)
  • Engineering Culture (17)
  • Critical Thinking (25)
  • Career Growth (57)
  • Life Experience and Thoughts (45)

推荐文章

  • 谈谈分布式锁
  • 常见分布式系统设计图解(汇总)
  • 系统设计中的快速估算技巧
  • 从链表存在环的问题说起
  • 技术面试中,什么样的问题才是好问题?
  • 从物理时钟到逻辑时钟
  • 近期面试观摩的一些思考
  • RSA 背后的算法
  • 谈谈 Ops(汇总 + 最终篇):工具和实践
  • 不要让业务牵着鼻子走
  • 倔强的程序员
  • 谈谈微信的信息流
  • 评审的艺术——谈谈现实中的代码评审
  • Blog 安全问题小记
  • 求第 K 个数的问题
  • 一些前端框架的比较(下)——Ember.js 和 React
  • 一些前端框架的比较(上)——GWT、AngularJS 和 Backbone.js
  • 工作流系统的设计
  • Spark 的性能调优
  • “残酷” 的事实
  • 七年工作,几个故事
  • 从 Java 和 JavaScript 来学习 Haskell 和 Groovy(汇总)
  • 一道随机数题目的求解
  • 层次
  • Dynamo 的实现技术和去中心化
  • 也谈谈全栈工程师
  • 多重继承的演变
  • 编程范型:工具的选择
  • GWT 初体验
  • java.util.concurrent 并发包诸类概览
  • 从 DCL 的对象安全发布谈起
  • 不同团队的困惑
  • 不适合 Hadoop 解决的问题
  • 留心那些潜在的系统设计问题
  • 再谈大楼扔鸡蛋的问题
  • 几种华丽无比的开发方式
  • 我眼中的工程师文化
  • 观点的碰撞
  • 谈谈盗版软件问题
  • 对几个软件开发传统观点的质疑和反驳
  • MVC 框架的映射和解耦
  • 编程的未来
  • DAO 的演进
  • 致那些自嘲码农的苦逼程序员
  • Java 多线程发展简史
  • 珍爱生命,远离微博
  • 网站性能优化的三重境界
  • OSCache 框架源码解析
  • “ 你不适合做程序员”
  • 画圆画方的故事

近期评论

  • + 1.943624 BTC.NEXT - https://graph.org/Ticket--58146-05-02?hs=9a9c6f8dfe3cdbe0074006e3e640b19b& on 所有文章
  • Anonymous on 闲聊投资:亲自体验和护城河
  • 四火 on 关于近期求职的近况和思考
  • YC on 关于近期求职的近况和思考
  • mafulong on 常见分布式基础设施系统设计图解(四):分布式工作流系统
  • 四火 on 常见分布式基础设施系统设计图解(八):分布式键值存储系统
  • Anonymous on 我裸辞了
  • https://umlcn.com on 资源链接
  • Anonymous on 我裸辞了
  • Dylan on 我裸辞了
© 2025 四火的唠叨 | Powered by Minimalist Blog WordPress Theme