JVM 平台上的各种语言的开发指南2
Groovy
“Groovy有超过Java将能够提供的甜点,例如它具有轻易地在宿主程序中嵌入并编译,以提供定制业务规则的能力,还有它如何为领域特定语言(Domain-Specific Language)提供优雅,简洁并且可读性好的语法的能力.”
Guillaume Laforge,
Groovy的项目带头人
Groovy入门
Groovy并不像我们在这个报告中涵盖的一些语言那样具有冒险性质,但绝对是你应该感兴趣的一种JVM语言。它已经成为了一种受到开发者信任的成熟选择,这时Java开发商的伤害,以及动态类型这些都不是问题。
无论如何,我都不会是那种争论何时给玩转一种编程语言一次机会的人。
Java变得过分充实
Java开发者可以在Groovy中深入编程并且变得多产。匹配Java的语言有希望或者这看起来将会是未来的趋势,2.0发行版中已经加入了Java7项目的Coin增强.另外Groovy还使得日常使用Java遇到的坎坷变得平滑。安全的导航(?.)以及 Elvis(?:)都算是很棒的例子。
// streetName will be null if user or // user.address is null - no NPE thrown def streetName = user?.address?.street // traditional ternary operator usage def displayName = user.name ? user.name : "Anonymous" // more compact Elvis operator - does same as above def displayName = user.name ?: "Anonymous"
“Groovy是一种多样性的JVM语言.使用一种同Java相近的语法,这种语言是易学的,并且允许你编写从脚本到完整的应用程序代码,包括强大的 DSL(领域特定语言)。Groovy很可能是JVM上唯一使得运行时的元编程、编译时的元编程、动态类型以及静态类型容易处理的语言。”
CÉDRIC CHAMPEAU,
Groovy中的高级软件工程师
闭包(Closure)
我们预期Groovy将止步于句法功能,然而我们却在文档中又发现“闭包”。为什么称这超越了我们的预期呢,因为Groovy函数值中的一等公民、更高级别的函数以及lambda表达式,这些都得到了支持。
square = { it * it } // ‘it’ refers to value passed to the function [ 1, 2, 3, 4 ].collect(square) // [2, 4, 9, 16]
标准库对于闭包恰到好处的应用使得使用它们成为一种享受,并且也证明了它们的实力。下面是使用闭包的语法糖作为方法后面参数的好例子:
def list = ['a','b','c','d'] def newList = [] list.collect( newList ) { it.toUpperCase() } println newList // [A, B, C, D]
集合
几乎所有的应用程序都依赖于集合。不幸的是集合大量的戳到了Java的痛处。而如果你怀疑我的这种说法,请尝试做一些有趣的JSON操作。Groovy为集合的定义将原有的语法打包,并且为了强大的可操作能力,着重使用了闭包。
def names = ["Ted", "Fred", "Jed", "Ned"] println names //[Ted, Fred, Jed, Ned] def shortNames = names.findAll { it.size() <= 3 } println shortNames.size() // 3 shortNames.each { println it } // Ted // Jed // Ned
静态类型
人们常常振奋于动态语言,因为你用很少的代码就能获得更多的功能。这常常很少被理解,而剩下的被带回去维护。因此我们能够看到越来越多的动态语言获得静态类型,而且反之亦然。
Groovy2.0加入了静态类型
静态类型缘何且如何提升了Groovy?
“静态检查使得从Java到Groovy的转型的之路更加的平滑。许多人加入(后续还有更多人加入)Groovy,因为它轻量级的语法,以及所有被移除的 样板,但是,举个例子,不想(或者不需要)使用动态特性。他们往往很难理解Groovy编译时不像他们以前那样抛出错误,因为他们实在是不能理解 Groovy是一门动态语言。对于他们来说,我们现在有了@TypeChecked。第二个原因是性能表现,由于Groovy仍然支持更老的JDK(现在 是1.5),而动态调用支持对它们不可用,因此为了代码的关键性能部分,你可以有静态的编译了的代码。还要注意的是,静态编译对于那些想要免受猴急修补 (monkey patching)的框架开发者来说是有趣的(基于能够在运行时改变一个方法的行为这一事实)。”
Cédric Champeau
Groovy的高级程序工程师
Groovy也不例外,静态检查可以通过相关代码中的@Typechecked注解实现.
import groovy.transform.TypeChecked void someMethod() {} <&br> @TypeChecked void test() { // compilation error: // cannot find matching method sommeeMethod() sommeeMethod() def name = "Marion" // compilation error: // the variable naaammme is undeclared println naaammme }
你最喜欢的,用Groovy写成的应用程序/框架/库是什么?还有为什么你对它饱含激情?你能说出超过一种么?
“那很简单,我最感到热情的就是Griffon,一个基于JVM的桌面应用程序开发平台。Groovy被用于作为框架中的原生语言,而其他JVM语言可能 也被这样对待。对于web开发,没有Grails我不会知道我会怎样编写web应用,简单来说这东西不用提刷新,还有点意思。Gradle是我工具箱中另 外一个宝贝,不管什么时候一有机会我就用它。最后,Spock展示了Groovy编译器的强大,还有AST表达式通过一个简单但是十分强大的测试DSL进 行处理。”
Andres Almiray,
Groovy 提交贡献者
Fantom
“Fantom是一个优雅的,强烈关注并发和可移植性的新一代语言。不可变性深深融入了Fantom 的类型系统,并且并发使用了演员(actor)模型。Fantom是着意于轻便性来设计的,并且对于Java VM和JavaScript/HTML都有产品质量级别的实现。Fantom务实于关注静态类型的风格,但是也能轻易的允许进行动态编程。它是一种面向对 象语言,但也包含了第一类函数(first class functions),并且许多的标准API都应用了闭包。”
Brian Frank,
Fantom创始人
Fantom入门
Fantom同我们在这个报告中观察的大部分其他语言有一点点不同之处,它的目标是多平台。基于JVM、.Net和JavaScript的编译现在都已经提供支持了,并且鉴于他们已经就位的基础设施,它应该也可能瞄准了其它的目标平台。
但是尽管可移植性和平台成熟度因素是Fantom作者(Brian 和 Andy Frank)考虑的重要问题,它也不是他们要定义这门语言的出处。他们声称Fantom是一个实用的语言,就是为了干实事的。
第一步要做的就是设置环境还有工具。幸运的是,Fantom使得这对于我们来说很容易。Xored搞了一个称作F4的基于Eclipse的IDE,它包含了我们需要让Fantom运行起来的一切。
Pod/脚本(Script)
Fantom可以将文件作为脚本执行,你只需要在文件中放置一个带有main方法的类,并且有一个可执行的扇子(fan)就可以运行它了。
class HelloWorldishScript { static Void main() { echo("Woah! Is it that easy?") } }
然而那不是构建Fantom程序的主要方法。对于大型的项目和提前编译好模块的产品级系统,称作Pod,是使用Fantom的构建工具创建的。
构建是通过一个构建脚本编排的,它本质上是Fantom代码的另一块。下面是HTTP服务器样本Fantom实现的构建脚本:
using build class Build : build::BuildPod { new make() { podName = "FantomHttpProject" summary = "" srcDirs = [`./`, `fan/`] depends = ["build 1.0", "sys 1.0", "util 1.0", "concurrent 1.0"] } }
这里有几个事项需要注意,例如依赖规范允许我们用比jar包更少烦恼的方式,更加容易的构建更大型的系统。另外,一个Pod不单单只定义部署命名空间,还 有类型命名空间,统一和简化了两者。现在你可以发现我们的服务器依赖的Pod:sys、util和concurrent。
Fantom是如何出现支持多个后端(.net,JavaScript)的创意的?
“在过去的生涯中,我们使用Java构建产品,但是有许多想要将解决方案卖到.NET商店中的问题。因此我们设计了Fantom来同时针对两个生态系统。 而当我们开始开发现在的产品时,我们将Fantom转入一个新的方向,目标是我们运行在JVM上的后端使用一种语言和代码库,而我们的前端运行在 HTML5浏览器上面。多年来,这在目前已经成为了一个非常成功的策略。”
Brian Frank,
Fantom创始人
标准库/Elegance
Fantom的身份不仅仅只是基于JVM平台(或者事实上是任何其它的平台)上的一种语言,而它自身更像就是处在JVM之上的一个平台。平台提供了API,而Fantom确保了API的精彩和优雅。在最基础的层面上它提供了几种文法,像下面这样的:
- Duration d := 5s
- Uri uri := `http://google.com`
- Map map := [1:"one", 2:"two"]
拥有一个周期文法( duration literal)是它的一个微不足道的小细节,但是当你想要设置一个延时操作的时候,你就能感觉到好像已经有人帮你考虑到了这个场景。
IO的API涉及到一些基础的类,如Buf、File、In/OutStreams,它们使用起来令人很愉悦。网络互通功能也提供 了,还有JSON的支持,DOM操作和图形库。重要的东西都为你而存在着。Util这个pod也包含了一些很有用的东西。代替一个拥有main方法的类, 文件服务器扩展了AbstractMain类,并且能自由的传递参数、设置日志。另一个使用起来令人很愉悦的API是Fantom的并发框架,
但我们将只 用几分钟来谈论一下它。
互操作(Interop)
所有构建于JVM平台之上的语言都提供了一些与原生Java代码协同工作的能力。这对于利用Java的庞大生态系统起到了关键作用。也就是说,要创建一个 比Java好的语言很容易,而创建一个比Java提供的相当得体的互操作更好的语言就难了。那部分归因于对集合的处理(它有点了老旧且平淡,而且有时候使 用起来有点儿痛苦)。
Fantom提供了一个Interop类,它有一个toFan方法和一个toJava方法,用于来回转换类型。
// socket is java.net.socket InStream in := Interop.toFan(socket.getInputStream) OutStream out := Interop.toFan(socket.getOutputStream)
这里你可以发现我们有了原生的Java Socket,它自然的为我们提供了Java的Input和OutputStream。
使用Interop将他们转换成与Fantom地位相同,并且稍后就使用它们。
静态和动态类型?
另一个任何语言都要审查的主题是这个语言是否提供静态/动态类型的支持。
Fantom在这一点处在中庸的位置,并且我们很喜欢这一点。属性域(Field)和方法带有强静态了性的特性。但是对于本地变量,类型就是被推断出来的。这导致了一种直观的混合,方法约束是被拼凑出来的,但是你也并不需要给每一样事物都赋上类型。
自然的,在Fantom中有两种方法调用操作。点(.)调用需要通过编译器检查并且是强类型的,箭头(->)调用操作则不是。这就从一个动态类型的语言中获得了鸭式类型(duck-typing)还有你想要的任何东西。
不可变性(Immutability)&并发(Concurrency)
Fantom提供了一个演员(Actor)框架来处理并发。消息传递和链式异步调用很容纳入代码中。为了创建一个演员(它将由某一种ActorPool支 持,而后通过一个线程池获得),你需要扩展一个Actor类,并且重写(奇怪的是你必须明明白白的给override关键词框定类型)receive方 法。
请注意避免在线程之间分享状态,Fantom将坚持要你只传递不可变的消息给actor。不可变性在设计时就构建到了语言之中,因此你可以构建所有的属性域都是常量的类。编译器将验证为actor准备的消息事实上是不可变的,否则将抛出一个异常。
比较酷,而且难于发现的一点就是,如果你真心需要一个可变的对象,你可以把它封装到一个Unsafe中(不,这不是那个不安全的概念,而是Fantom中的Unsafe).
while(true) { socket := serverSocket.accept a := ServerActor(actorPool) //wrap a mutable socket to sign that we know what are we doing a.send(Unsafe(socket)) }
稍后你可以把原来的对象再取回来.
override Obj? receive(Obj? msg) {
while(true) { socket := serverSocket.accept a := ServerActor(actorPool, socket) a.send(“handleRequest”) }
// Unsafe is just a wrapper, get the socket log.info("Accepted a socket: $DateTime.now") Socket socket := ((Unsafe) msg).val ... }
"你应该考虑吧Unsafe作为最后的杀手锏——因为它会侵蚀Fantom中的整个并发模型.如果你需要传递可变的状态 b/w Actor——你应该使用序列化——它是一个内建的特性: http://fantom.org/ doc/docLang/Actors.html#messages"
Andy Frank,
Fantom创始人
这意味着,适当的解决方案在这里就会像这样:
while(true) { socket := serverSocket.accept a := ServerActor(actorPool, socket) a.send(“handleRequest”) }
这样我们就能够为接下来为对那个的IO操作存储套接字对象(socket),而我们不需要再去传递任何不可变的消息了.顺便获得的好处是,这样代码看起来更好了。
函数(Functions)& 闭包(Closures)
Fantom是一种面向对象的语言,它像许多其他的现代编程语言一样,将函数做为了头等公民。下面的例子展示了如何创建一个Actor,我们将含蓄指定一个接收函数向其发送几次消息。
pool := ActorPool()
a := Actor(pool) |msg|
{
count := 1 + (Int)Actor.locals.get("count", 0)
Actor.locals["count"] = count
return count
}
100.times { a.send("ignored") }
echo("Count is now " + a.send("ignored").get)
Fantom’s的语法十分的友好,与其他语言没太大出入。我们大概已经发现,它用":="来进行变量的赋值,这对我来说是一种灾难(对这样的小的语法细节,我并不十分喜欢)。然而IDE对此支持得十分友好,每当你犯此错误的时候,它都会提醒你。
一些小东东
在我们研究Fantom的整过过程中,一些促使这门语言更加棒的小东东会令我们惊喜.例如,支持null的类:它们是能够声明一个接受或者不接受null作为参数的方法的一种类型.
Str // never stores null Str? // might store null
通过这种方式,代码不会受到空(null)检查的污染,并且同Java的互操作变得更加的简单了.
值得一提的还有另外一种特性.那就是带有变量插值(Variable interpolation)的多行字符串(Multi-line String):
header := "HTTP/1.1 $returnCode $status Server: Fantom HTTP Server 1.0 Date: ${DateTime.now} Content-type: ${contentType} Content-length: ${content.size} ".toBuf
参数可以有默认值,支持混合和声明式编程,还有运算符重载.每一样东西都各得其所.
只有一件东西我们没有看到并感到是一种缺陷,那就是元组(tuples).然而我们只在想要多返回(mutilple return)时才需要那个东西,因此使用一个列表(list)就足够了.
Clojure
“我着手创建一种语言,意在应对我在使用Java和C#编写的一些类型的应用程序——像广播自动化、调度以及选举系统之类那些东西——它们许多都需要解决 的并发问题.我发现只用面向对象编程和用那些语言的并发方法,对于处理这些类型的问题并不怎么够好——它们太难了。我是List的拥护者,还有其它的函数 式语言,而我想要做的就是解决那些问题,创造一种立足于实际的语言,再也不用拿Java来编程了.”
Rich Hickey,
Clojure创始人在2009年InfoQ访谈中
Clojure 入门
Clojure 第一次出现在2007年 。和一些成熟的语言相比,它相对是新的。Rich Hickey创造了它。它作为Lisp方言在JVM上面。版本1.0出现在2009年。它的名字是一个双关语在C(C#), L (Lisp) and J (Java)。当前Clojure的1.4。Clojure是开源的(发布在Eclipse 公共许可证 v1.0—EPL)
当你开始阅读Clojure的文档的之后,我们决定开始配置环境变量。我们使用Leiningen(它是一个为Clojure准备的构建工具) 。对 Clojure来说,Leiningen (或者缩写Lein) 能够执行大部分任务。它能够执行我们期待的来自Maven相关的例如:
- 创造一个项目骨架 (想一想: Maven 原型)
- 操作依赖
- 编译 Clojure代码到JVM classes类
- 运行测试
- 发布构件到一个中央仓库
假如你使用过Maven, 你会感觉使用Lein非常的舒适。事实上,Lein 甚至支持Maven的依赖。然而两者的主要不同点在于 Lein的项目文件由 Clojure写成。然而Maven使用XML(pom.xml)。尽管有可能去开发用Clojure没有其它的,我们不得不承认Lein是一个非常受欢迎的增加物。它真得是某些事情变得更加简单。
开始得到项目骨架,你能够做像下面这样:
$ lein new clojure-http-server
集成开发环境(IDE)的支持
准备好基本的项目结构以后,就可以开始编辑代码了.如果你是Eclipse的长期使用者,你首先要做的一件事情就是找到一个可以处理 Clojure 的Eclipse插件.它应该在一个成熟的工作空间中提供语法高亮和代码补全功能.幸运的是Eclipse中有了一个叫做 CounterClockWise的Clojure插件.只要安装了这个插件,就可以在Eclipse中新建Clojure项目了.
这样很不错,我们不需要再去管将我们已经使用过的,前面章节提到的Lein,在命令行中创建的Clojure项目然后导入到 Eclipse中这种麻烦事了。我们预计CounterClockWise插件提供了像Eclipse的Maven插件有的在pom.xml和 EclipseGUI之间两种交互方式,所提供的功能.
仅仅为了好玩,我们也看了看Clooj,它是用Clojure本身开发的一个轻量级的Clojure IDE.下载和运行它都很容易,不过我们发现它同Eclipse相比,就有点黯然失色了.
最后我们用了用最难的方式开发Clojure程序——只在命令行中使用Lein,还有可靠的GVIM作为文本编辑器——主要是想看看Lein是如何详细工作的.
交互式解释器(REPL)
像众多的函数式语言, Clojure提供的命令行shell可以直接执行Clojure语句。这个shell对开发者非常方便,因为它在开发中不仅允许你测试一小端代码,而且允许你运行程序的一部分。
这或许对用Python,Perl开发的码农没什么新鲜的,但对Java开发者来说,这无疑带来更新鲜、更交互的方式来写代码。
函数式编程——另外一种思考方式
Clojure是一种非常类似于Lisp和Scheme的函数式编程语言.函数式范式同那些习惯于Java的面向对象方式并且习惯于其副作用的方式非常不同.
函数式编程推崇:
- 很少或者完全没有副作用
- 如果使用相同的参数区调用,函数就永远返回同一个结果(而不是依赖于对象状态的方法)
- 没有全局变量
- 函数是第一位的对象
- 表达式懒计算(Lazy evaluation of expression)
这些特性并不是Clojure独有的,而是总体上对函数式编程都要求的:
(defn send-html-response "Html response" [client-socket status title body] (let [html (str "" body "")] (send-http-response client-socket status "text/html" (.getBytes html "UTF-8")) ))
同Java的互操作性
Clojure提供了优秀的同Java库的互操作功能.事实上,对于一些基本的类,Clojure并没有提供它自己的抽象,而是超乎你预期的直接使用了Java类来代替.在这个HTTP服务器的示例中,我们从Java中获取了像Reader和Writer这样的类:
(ns clojure-http-server.core (:require [clojure.string]) (:import (java.net ServerSocket SocketException) (java.util Date) (java.io PrintWriter BufferedReader InputStreamReader BufferedOutputStream)))
创建和调用Java对象是非常直截了当的.而实际上有两种形式(在这个优秀的Clojure介绍中描述到了):
(def calendar (new GregorianCalendar 2008 Calendar/APRIL 16)) ; April 16, 2008 (def calendar (GregorianCalendar. 2008 Calendar/APRIL 16)) Calling methods: (. calendar add Calendar/MONTH 2) (. calendar get Calendar/MONTH) ; -> 5 (.add calendar Calendar/MONTH 2) (.get calendar Calendar/MONTH) ; -> 7
下面是一个实际的样例:
(defn get-reader "Create a Java reader from the input stream of the client socket" [client-socket] (new BufferedReader (new InputStreamReader (.getInputStream client- socket))))
然而,对于一些结构,我们决定要使用到Clojure的方式.原生的Java代码使用StringTokenizer,这样做违背了不可变对象的纯函数式 原则,还有无副作用的原则.调用nextToken()方法不仅有副作用(因为它修改了Tonkenize对象)并且使用同一个(或许是不存在的)参数也 会有不同的返回结果.
由于这个原因,我们使用Clojure的更加"函数式"的Split函数:
(defn process-request "Parse the HTTP request and decide what to do" [client-socket] (let [reader (get-reader client-socket) first-line (.readLine reader) tokens (clojure.string/split first-line #"\s+")] (let [http-method (clojure.string/upper-case (get tokens 0 "unknown"))] (if (or (= http-method "GET") (= http-method "HEAD")) (let [file-requested-name (get tokens 1 "not-existing") [...]
并发(Concurrency)
Clojure从一开始设计对并发很上心,而不是事后诸葛亮.使用Clojure编写多线程应用程序非常简单,因为所有的函数默认都实现了来自Java的Runnable和Callable接口,自身得以允许其任何方法在一个不同的线程中运行。
Clojure也提供了其它特别为并发而准备的结构,比如原子(atom)和代理(agent),但是我们在这个HTTP服务器示例中并没有使用它们,而是选择熟悉的Java的Thread.
(defn new-worker "Spawn a new thread" [client-socket] (.start (new Thread (fn [] (respond-to-client client-socket)))))
有关方法使用顺序的问题
我们认识到的一件事情是在源代码文件中,方法的顺序是严格的。函数必须在它们第一次被使用之前被定义。另外一种选择是,你可以在一个函数被实际定义之前利用一种特殊的声明格式,来使用函数。这让我们想起了C/C++的运作方式,它们使用头文件和函数声明。