Clojure Functional Programming For The Jvm

Clojure-JVMλ语言(1) 综述

http://roysong.iteye.com/blog/1222746

原作者: R. Mark Volkmann
原文地址:http://java.ociweb.com/mark/clojure/article.html
作者:R. Mark Volkmann
译者:RoySong

简介

这篇文章的目的是给Clojure做一个广泛公正的介绍,以简要的形式对多个特性进行了介绍.可以根据兴趣选看其中的章节.

对本文有任何意见或者建议发送邮件到 moc.bewico|kram#moc.bewico|kram ,这篇文章的最新版本会在 http://www.ociweb.com/mark/clojure/ 刊载,上面有更新的日期,同样,你也可以在 http://www.ociweb.com/mark/stm/ 看到关于Clojure的软件事务内存特性(STM)的文章。

这篇文章中的代码示例通常采用 “;注释-〉结果”的形式来展示函数调用的结果以及输出,比如:
Clojure代码

(+ 1 2) ; showing return value -> 3
(println "Hello") ; return value is nil, showing output -> Hello

函数式编程

函数式编程是一种强调“第一类”(first-class)“纯”函数的编程模式。它是根据λ运算原理演化而来的。

所谓的“纯函数”是指传给同样的参数总是会返回同样结果的函数,而不是那些状态随着时间改变的函数。纯函数的特性使它更加容易理解、调试和测试。它们没有诸如:改变了某个全局状态,执行了各种I/O操作,数据库更新或者文件读写这类型的副作用(side effects)。状态通过保存在栈(stack)中的函数参数来维护,而不是通过保存在堆(heap)的全局变量来控制。这样就允许函数能够被重复执行而不用担心影响到全局状态(这是一个重要的特性,在之后我们讨论事务时会谈到)。它也为编译器提升性能打开了一扇门,以自动组织和实现并发的形式,尽管当前而言自动并发化还未实现。

在实际中,应用程序需要产生某些副作用。 Simon Peyton-Jones,函数式编程语言Haskell的主要贡献者,曾经说过:“到最后,任何程序都会去改变状态。没有副作用的函数是某种类型的黑匣子,你唯一能说的就是匣子越来越烫”。 (http://oscon.blip.tv/file/324976 ) 关键在于限制副作用,清晰的标识它们,避免让它们散落在代码的各个角落

支持“第一类函数”的编程语言允许采用变量绑定函数,并把它传递给其他函数或者作为其他函数的返回值。这种允许返回值为函数的特性支持某些行为的延迟执行。采用其他函数作为参数的函数被称为“高层函数”(higher-order functions),从某种意义上来说,它们的行为取决于作为参数的其他函数。被传入的参数函数可以被执行任意次数,包括完全不执行。

函数是编程语言中的数据(Data)通常是不变的,这样在多线程并发使用数据时就不用加锁。不能改变的数据自然就没必要加锁。随着多处理器变得越来越普遍,函数式编程能简单支持并发这一点或许会成为最大的优势。

 如果上面这些听起来还算有趣,而你准备开始尝试函数式编程,那么,准备接受一个陡峭的学习曲线吧。函数式编程的很多概念并不比面向对象编程更难,它只是完全不同。介于上文提到的优点,我认为花时间去学习函数式编程是值得投入的。

 函数是编程语言包括

  • Clojure
  • Common Lisp
  • Erlang
  • F#
  • Haskell
  • ML
  • OCaml
  • Scheme
  • Scala

Clojure 和 Scala是运行在java虚拟机(JVM)上面的。其他能够运行在JVM上面的函数式编程语言还有: Armed Bear Common Lisp (ABCL) , OCaml-Java 和 Kawa (Scheme) 。

Clojure 综述

 Clojure是运行在JVM(java5或者更高)上面的一门动态的、函数式编程语言,并且提供和java的互操作性。这门语言的主要目的就是为了让应用能够更便捷地实现多线程并发操作数据。

 很明显Clojure同单词“closure”很像。这门语言的作者,Rich Hickey,解释了这个命名产生的方式:“我想要在命名中包含C (C#), L (Lisp) 和 J (Java)。当从closure的双关语中提出Clojure时,面对有效的域名和大片空白的google空间,做出这个决定就很简单了。”

不久之后Clojure同样可以运用于.net平台,ClojureCLR是Clojure基于Microsoft Common Language Runtime的实现,当这篇文章撰写时,它刚刚发布了alpha版本。

在2011年7月,ClojureScript发布了,它能够将Clojure代码编译为JavaScript。参见 https://github.com/clojure/clojurescript

Clojure是一门基于 Eclipse Public License v 1.0 (EPL)协议的开源发布语言。EPL是一个非常宽松的协议,详情参见http://www.eclipse.org/legal/eplfaq.php

运行在JVM上面意味着可移植性、稳定性、好的性能以及安全。这同样提供了对java已有库的访问权限,比如文件I/O,多线程,GUI,web应用等等。

在Clojure中的每个操作都是函数、宏或者特殊form。几乎所有的函数和宏都是以Clojure源代码的形式来实现。而函数和宏的区别将在下文详细解释。特殊form由Clojure的编译器识别,并且不能以Clojure源代码的形式实现。有很小一部分特殊form和新的form是不能实现的,它们包括:catch , def , do , dot ('.'), finally , fn , if , let , loop , monitor-enter , monitor-exit , new , quote , recur , set! , throw , try 和 var 。

Clojure提供了很多函数来对“序列”(sequences,集合的逻辑视图)进行便捷操作。很多东西都可以被视为序列,包括Java collections, Clojure-specific collections, strings, streams, directory structures 和XML trees。Clojure能够采用高效的方式从已有的集合中创建新集合的实例,因为这些集合都是持久化数据结构( persistent data structures )。

 Clojure提供三种方式来安全共享可变的数据,这些可变数据意味着对不变数据的可变引用。通过 Software Transactional Memory (STM)机制,采用 Refs 可以提供对多层共享数据的同步访问。 Atoms 和 Agents 都提供了对单层共享数据的同步访问,它们将在“引用类型”("Reference Types ")这一节被详细阐述。

Clojure是一种 Lisp 方言。然而,它和老Lisp有着诸多不同。比如:老Lisp中采用car函数来获取list中的第一个元素,而Clojure中采用first函数,如同Common Lisp一样。Clojure和老的Lisp不同处列表参见http://clojure.org/lisps

Lisp的语法由于对前置操作符和成对括号的使用让很多人喜欢,也让很多人痛恨。如果你属于后者,那么请注意以下事实:很多文本编辑器和IDE都能够高亮成对的括号,所以你没必要去计算括号的层数以保证它们总是闭合的。Clojure的函数调用并不像java那样杂乱无章。比如,某个java方法调用如下:
Java代码

methodName(arg1, arg2, arg3);

而对应的Clojure函数调用看起来是这个样子的:
Clojure代码

(function-name arg1 arg2 arg3)

括号跑到了函数名前面,参数间的逗号都没有了,语句最后的分号也没有了。语法上这被称为一个“form”,事实上,Lisp中的任何东西都包含form。注意,Clojure中的命名约定是采用小写单词并以“-”分割,不像java的驼峰命名法。

在Clojure中定义函数同样非常简约。Clojure的println 函数会在每个参数的对应输出后面加上空白,为了避免这一点,我们将所有的参数传给str函数,然后将这个函数的返回值传给println 。
Java代码

// Java  
public void hello(String name) {  
    System.out.println("Hello, " + name);  
}

Clojure代码
; Clojure  
(defn hello [name]  
  (println "Hello," name));add more space  
 
(defn hello [name]  
  (println (str "hello," name)));use str function

Clojure广泛应用了“延迟求值”(lazy evaluation),这个机制允许函数仅当需要它的返回值时才进行调用。“延迟序列”(Lazy sequences)是直到实际使用时,才会计算集合的结果。这样就能支持无穷集合的有效创建。

Clojure代码的处理分三个阶段:读取时、编译时和运行时。在读取时,读取器会阅读源代码并将它转换为某个数据结构,大部分是一个list包含许多list,其中每个list又包含很多list这种形式。在编译时,这个数据结构会被编译为java字节码。在运行时,字节码被执行。函数仅在运行时被调用。宏和特殊结构在调用上看起来跟函数一样,但实际上在编译时它们就已经被展开为新的Clojure代码了。

是否Clojure代码很难以理解?想象一下当你阅读java代码时,遇到某些语法元素,比如:声明、for循环和匿名类,你不得不停下来为它们的含义而迷惑。当然,对于有志于成为职业java开发者的人来说,以上这些含义都是明确无疑的。同样地,对于能有效阅读和理解Clojure代码的人来说,Clojure的语法部分也是明确无疑的。包含对let , apply , map , filter , reduce以及匿名函数的舒适运用的例子将在接下来为大家展示。

入门指南

Clojure是一门相对较新的语言。它经过数年的工作后在2007年10月16号首次发布,Clojure主文件的下载被称为 "Clojure proper" 或者 "core". 它可以从 http://clojure.org/downloads 下载获得,或者也可以采用 Leiningen 来获得。最新的代码版本可以从Git repository中获得.

"Clojure Contrib " 是Clojure的组件库.库中的组件大多是成熟、通用的,甚至某些最终会被包含到Clojure的系统中去。然而,库中同样存在不成熟、不通用,也不适合包含到Clojure中去的组件。这是一个鱼龙混杂的仓库。有关这个仓库的详细文档,请参见http://richhickey.github.com/clojure-contrib/index.html

有三种方式能够为Clojure发布获得一个.jar文件。第一,下载一个预编译的.jar文件。第二,一个采用Maven来构建的源代码。Maven能够从 http://maven.apache.org/ 获得。执行的命令是"mvn package ".第三,一个采用Ant来构建的源代码。Ant可以从 http://ant.apache.org/ 获得。Ant的执行命令是 "ant -Dclojure.jar={path

} "。

为了获得最新的构建版本,假设你已经安装了 Git 和 Ant ,执行如下的脚本来检索和构建Clojure本身和组件库:
Git代码

git clone git://github.com/richhickey/clojure.git  
cd clojure  
ant clean jar  
cd ..  
git clone git://github.com/richhickey/clojure-contrib.git  
cd clojure-contrib  
ant -Dclojure.jar=../clojure/clojure.jar clean jar

然后,创建一个脚本来启动Read/Eval/Print Loop (REPL)和运行Clojure程序。这个通常被命名为“clj”。REPL的使用稍后将会解释。windows系统下下面一行脚本语句就可以满足刚刚两个要求(在UNIX, Linux and Mac OS X下面,将%1 改为$1):

java -jar path/clojure.jar %1

这条脚本语句假设在环境变量path中能够找到java的目录。如下所述可以让这条脚本语句更有用:
添加经常使用的jar包比如 "Clojure Contrib "和数据库驱动到 classpath (-cp )
添加可编辑、自动完成和交互会话命令行特性,采用 rlwrap (supports vi keystrokes) 或者 JLine
采用开始脚本来设置特殊符号(比如*print-length* 和*print-level* ),引入常用但不在java.lang包中的java类,加载常用但不在 clojure.core命名空间中的Clojure函数,和定义常用的自定义函数。
采用脚本来启动REPL将会在接下来的章节中讨论到。运行Clojure源文件,通常以.clj后缀,的脚本如下:
Java代码

clj source-file-path

更多的细节, 参见 http://clojure.org/getting_startedhttp://clojure.org/repl_and_main 。同样地, Stephen Gilardi 在 http://github.com/richhickey/clojure-contrib/raw/master/launchers/bash/clj-env-dir 提供了一个现成的脚本。

为了发挥多处理器的最大优势,可能需要采用 "java -server … "这种方式来启动Clojure。

传递给Clojure程序的命令行参数是有效的,参数会预定义绑定到*command-line-args*上面。

Clojure语法

Lisp方言拥有非常简洁,某些地方可以称作优美的语法。数据和代码拥有同样的结构和表现形式,list的嵌套能够在内存中很自然的如同树状呈现。

(a b c)的含义是一个名叫a的函数接收了b和c两个参数,然后被调用。
如果要把它作为数据处理而不是函数调用,则需要在前面加上引号。
'(a b c) 或者 (quote (a b c))代表一个list,包含a、b和c。除了某些
特殊的情况,一般语法就是这样的。而不同的情况的数目取决于采用哪种方言。

特殊的情形可以以某些语法糖的形式被察觉。拥有更多的语法糖,代码就会变得更短,代码的读者就需要学习和记忆更多。这是一种微妙的平衡。大部分的语法糖都拥有同名可替代的函数。我把决定权留给你自己,去采用或多或少的语法糖。

下面的列表简单地描述了Clojure代码中会遇到的特殊情况。这些都会在接下来的章节中会被详细描述,所以,现在你不用完全弄明白它们。

目的 语法糖 函数
注释 ; text - for line comments (comment text - for block comments) macro
character literal (采用Java char 类型) \char \tab ;\newline \space ;\uunicode-hex-value (char ascii-code ) ;(char \uunicode )
字符串 (采用 Java String 对象) "text " (str char1 char2 …) ;连接字符以及其他多种类型的值来创建一个字符串
关键字;一个被保留的字符串;同样名字的关键字指向同样的对象;通常被用于map的key :name (keyword "name ")
在当前命名空间中生效的关键字 ::name none
正则表达式 #"pattern " ;引用规则不同于函数form (re-pattern pattern )
视为空白符;有时会用在集合中来增加可读性 , (一个逗号) N/A
list - a linked list '(items )  (list items ) 
doesn't evaluate items evaluates items
vector - similar to an array [items ] (vector items )
set #{items } ;创建了一个 hash set (hash-set items ) ;(sorted-set items )
map {key-value-pairs } ;创建了一个 hash map (hash-map key-value-pairs ) ;(sorted-map key-value-pairs )
为符号或者集合添加元数据 #^{key-value-pairs } object (with-meta object metadata-map ) 
add metadata to a symbol or collection processed at read-time processed at run-time
从符号或者集合上获取映射的元数据(get metadata map from a symbol or collection) ^object (meta object )
获取某函数的参数数量(gather a variable number of arguments,in a function parameter list) & name N/A
conventional name given to function parameters that aren't used _ (an underscore) N/A
初始化一个java类;注意类名后面还有个句点 (class-name . args ) (new class-name args )
调用一个java方法 (. class-or-instance method-name args ) or (.method-name class-or-instance args ) none
调用数个java方法,将每个方法返回的结果作为下一个方法的第一个参数;每个方法在括号中拥有一个额外的参数;注意开头的双句点 (.. class-or-object (method1 args ) (method2 args ) …) none
创建一个匿名函数 #(single-expression ) ;use % (same as %1 ), %1 , %2and so on for arguments (fn [arg-names ] expressions )
dereference a Ref, Atom or Agent @ref (deref ref )
get Var object instead of the value of a symbol (var-quote) #'name (var name )
syntax quote (used in macros) ` none
unquote (used in macros) ~value (unquote value )
unquote splicing (used in macros) ~@value none
auto-gensym (used in macros to generate a unique symbol name) prefix # (gensym prefix ?)

Lisp方言采用前缀符号,而不同于一般的编程语言中,把符号比如+或者*放到中间。
举个例子,在java中,可能会这么写:a + b + c;而在Lisp的方言中,写法变为(+ a b c)。
这种写法的一个好处是,不用重复操作符就可以指定任意数量的参数,
而不像其他语言的基础操作符一样受限于两个操作数。

Lisp代码比其他语言要多很多括号的原因是它使用圆括号如同java使用大括号一般。举个例子,java的方法声明是用大括号包起来的,而Lisp函数定义中的表达式是用圆括号包起来的。

比较以下java和clojure的代码片段,作用都是定义一个简单函数并调用它。两者的输出都是"edray" 和 "orangeay".
Java代码

// This is Java code.  
public class PigLatin {  
 
    public static String pigLatin(String word) {  
        char firstLetter = word.charAt(0);  
        if ("aeiou".indexOf(firstLetter) != -1) return word + "ay";  
        return word.substring(1) + firstLetter + "ay";  
    }  
 
    public static void main(String args[]) {  
        System.out.println(pigLatin("red"));  
        System.out.println(pigLatin("orange"));  
    }  
}

Clojure代码

; This is Clojure code.  
; When a set is used as a function, it returns a boolean  
; that indicates whether the argument is in the set.  
(def vowel? (set "aeiou"))  
 
(defn pig-latin [word] ; defines a function  
  ; word is expected to be a string  
  ; which can be treated like a sequence of characters.  
  (let [first-letter (first word)] ; assigns a local binding  
    (if (vowel? first-letter)  
      (str word "ay") ; then part of if  
      (str (subs word 1) first-letter "ay")))) ; else part of if  
 
(println (pig-latin "red"))  
(println (pig-latin "orange"))

Clojure支持所有的公用数据类型比如说boolean(字面值是true和false),integers, decimals, characters (参见上面表格中的 "character literal") 和strings。它同样支持包含分子和分母的分数,并且能够在计算中不丧失精度。

符号(symbol)被用在命名某些东西上,这些名称的作用域是在某个命名空间中—或是被指定的命名空间,或是默认的命名空间。使用符号实际上是使用的符号所指向的值,如果要获取符号本身,必须采用‘号。

关键字(keyword)以冒号开头,被作为一个独一无二的标示符使用。例子包括map中的key,或者枚举的值(比如::red , :green 和 :blue )。

在Clojure中,以及其他的编程语言中,都有可能写出难以理解的代码。遵循少量的指导方针会带来巨大的差异。编写短小的、目标明确的函数使得它们便于阅读、测试和重用。经常采用“导出方法”(extract method)重构模式。深层嵌套的函数调用让人很难理解。如果有可能的话,限制嵌套的使用,通常采用let来将某个复杂的表达式变为数个不那么复杂的表达式。将某个匿名函数传给某个有名字的函数是常见的。然而,要避免将一个匿名函数传给另一个匿名函数,因为这样的代码是非常难以阅读的。

REPL

REPL意即读取(read)、求值(eval)、打印(print)的循环(loop)。这是Lisp方言中的一个标准工具,它允许用户在其中输入表达式,然后使输入内容被读取和计算,最后打印出得到返回的结果。这是一个对测试和增进了解程序非常有用的工具。

在命令行中运行之前我们建立的脚本,叫做“clj”那个,就可以启动REPL。然后就会出现"user=> "提示符,"=> "之前的部分代表当前的命名空间。在提示符后输入的内容将被求值,并打印结果在屏幕上。下面是个REPL输入输出的简单例子:
Clojure代码

user=> (def n 2)  
#'user/n  
user=> (* n 3)  
6

第一个参数def是个特殊的form,它不会被求值,而是采用它的字面值作为名字。
这个表达式的输出显示了在“user”命名空间中定义了一个名叫“n”的符号。

要查看某个函数、宏或者命名空间的文档信息,使用(doc name)。如果查看的是个宏,单词 "Macro"会迅速出现在一条在参数列表下面的线上。被查看的元素必须要提前加载(参见 require 函数)到当前的命名空间来。举个例子:
Clojure代码

(require 'clojure.contrib.str-utils)  
(doc clojure.contrib.str-utils/str-join) ; ->  
; -------------------------  
; clojure.contrib.str-utils/str-join  
; ([separator sequence])  
;   Returns a string of all elements in 'sequence', separated by  
;   'separator'.  Like Perl's 'join'.

要在所有的函数或者宏中去查找某些名字或者文档中包含某个字符串的文档,采用(find-doc "text")。

要查看某个函数或者宏的源代码,采用(source name)。source是一个定义在 clojure.contrib.repl-utils命名空间中的宏,这个命名空间会在REPL中自动加载。

要从文件中加载并运行form,采用(load-file "file-path")。通常这些文件采用 .clj扩展名。

在window系统下要离开REPL环境,按下ctrl+z然后回车或者直接ctrl+c都可以。在其它系统(包括UNIX, Linux 和 Mac OS X)下要离开REPL,按下ctrl+d就可以了。

绑定(bindings)

Clojure并不支持变量。与之替代的是绑定(bindings),跟变量很像,但在指定了值之后并不打算去改变。绑定包含全局绑定(global bindings)、线程本地绑定(thread-local bindings)、函数内本地绑定(local binding)或者某些form内的本地绑定。

特殊form def 会创建一个全局绑定并给予一个“顶级值”( root value),在所有的线程中全局绑定都会是这个值,除非重新指定了一个线程本地绑定。 def 同样可以用来改变某个已存在绑定的顶级值。然而,这种做法是会被鄙视的,因为它破坏了数据的不可变性。

函数的参数就是函数的本地绑定。
特殊form let会创建一个针对某个form的本地绑定。它的第一个参数是一个包含“名字/表达式”对的vector,
这些表达式会依次被求值,然后将执行的结果赋予它左侧的名字。这些绑定能够被参数vector后面的表达式所使用。
它们同样有可能被多次指定以改变它们的值。let剩下的参数由一系列采用本地绑定的表达式所组成。
在let作用域内调用的其他函数是无法感知到let的本地绑定的。

binding 宏类似于 let,它能暂时创建一个线程本地绑定覆盖掉 已存在的全局绑定。这个线程本地绑定的值可以在form内部以及内部调用的函数所感知到。当离开 binding form的范围,全局绑定就会恢复之前定义的值。

从Clojure1.3开始,仅仅声明为动态变量(dynamic vars)的绑定可以这么做了。下面的例子会演示如何声明动态变量。

let和binding的另一个区别在于,let顺序地指定绑定的值,后面绑定的值可以基于前面绑定来赋予,而binding则是并发指定绑定的值。

能够采用binding来绑定新线程本地值的符号有其约定的命名规则,这些特殊的符号前后都用*号包围。在这篇文章中出现的例子包括:*command-line-args* , *agent* , *err* , *flush-on-newline* , *in* , *load-tests* , *ns* , *out* , *print-length* , *print-level* 和 *stack-trace-depth*。采用这些绑定的函数会被其值的改变所影响。举个例子,改变*out*的值会影响 println 函数的输出目标。

下面的例子展示了 def , let 和 binding的用法:
Clojure代码

(def ^:dynamic v 1) ; v is a global binding  
 
(defn f1 []  
  (println "f1: v =" v)) ; global binding  
 
(defn f2 []  
  (println "f2: before let v =" v) ; global binding  
  (let [v 2] ; creates local binding v that shadows global one  
    (println "f2: in let, v =" v) ; local binding  
    (f1))  
  (println "f2: after let v =" v)) ; global binding  
 
(defn f3 []  
  (println "f3: before binding v =" v) ; global binding  
  (binding [v 3] ; same global binding with new, temporary value  
    (println "f3: in binding, v =" v) ; global binding  
    (f1))  
  (println "f3: after binding v =" v)) ; global binding  
 
(defn f4 []  
 (def v 4)) ; changes the value of the global binding  
 
(f2)  
(f3)  
(f4)  
(println "after calling f4, v =" v)

上面例子的输出如下:
Clojure代码
f2: before let v = 1  
f2: in let, v = 2  
f1: v = 1 (let DID NOT change value of global binding)  
f2: after let v = 1  
f3: before binding v = 1  
f3: in binding, v = 3  
f1: v = 3 (binding DID change value of global binding)  
f3: after binding v = 1 (value of global binding reverted back)  
after calling f4, v = 4

Clojure-JVMλ语言(2) 集合

博客分类: 翻译Clojure
clojurelispjava
原帖地址:http://java.ociweb.com/mark/clojure/article.html#Collections
作者:R. Mark Volkmann
译者:RoySong
集合(Collections)

Clojure提供了list, vector, set 和map集合类型。Clojure同样可以采用任何Java集合类,但这并不经常出现,因为Clojure本身的集合更适合于函数式编程一些。

Clojure的集合类型拥有完全不同于Java集合的特性。它们都是不可变的、多相的、持久化的。不可变意味着集合的内容不能够被改变。多相意味着集合 可以包含任意类型的对象。持久化意味着在集合的新版本创建时,旧版本依然被保存起来。Clojure采用了一种高效的方式来实现这个,即新旧版本共享内 存。举个例子,一个新版本的map包含了上千个键值对,但比起旧版本来只改变了其中一个键值对,在共享内存的情况下,只需要消耗一点点额外的内存就可以创 建新的版本。

存在很多核心函数来操作各种类型的集合…实在是太多以至于无法在这儿详细描述。它们的一个小子集将在接下来被描述。记住一点,Clojure的集合是不可变的,没有函数能够改变它们。与之替代的,有很多函数采用 persistent data structures 的特性高效地从一个已存在集合创建出一个新的来。同样的,一些函数操作某个集合(比如:vector)而返回另一个类型的集合(比如:LazySeq ),它们也拥有不同的特性。

警告:这一节包含学习Clojure集合的重要信息,然而,它有些单调无味,呈现出的是一个接一个的函数操作各种类型的集合。如果睡意弥漫,就跳到下一节的开头,以后再来看这一段算了。

count 函数

计算任意类型的集合包含的元素数量,例子如下:
Clojure代码

(count [19 "yellow" true]) ; -> 3

conj 函数

是conjoin的简写,能够为某个集合添加一个或者多个元素。新元素被添加到什么位置取决于集合的类型。这将在我们接触到特定集合类型时来解释。

reverse 函数

能够将集合中的元素倒序排列:
Clojure代码

(reverse [2 4 7]) ; -> (7 4 2)

map函数

为某集合的每个元素提供一个接受单一参数的指定处理函数,返回一个 lazy sequence。如果被处理的集合提供了每个元素,它同样也可以提供接受多参数的函数。如果这些集合拥有不同个数的元素,函数只会按照最短集合的长度来处理每个元素,其他集合中比最短集合长的元素都会被舍弃。举个例子:
Clojure代码

; The next line uses an anonymous function that adds 3 to its argument.
(map #(+ % 3) [2 4 7]) ; -> (5 7 10)
(map + [2 4 7] [5 6] [1 2 3 4]) ; adds corresponding items -> (8 12)

apply函数

会将某个集合的所有元素作为参数传给指定的函数,并返回指定函数的返回值。例子;
Clojure代码

(apply + [2 4 7]); -> 13

有很多函数是从集合中检索出某个特定值,举例如下:
Clojure代码

(def stooges ["Moe" "Larry" "Curly" "Shemp"])
(first stooges) ; -> "Moe"
(second stooges) ; -> "Larry"
(last stooges) ; -> "Shemp"
(nth stooges 2) ; indexes start at 0 -> "Curly"

也有很多函数是从集合中检索出特定的多个值,举例如下;
Clojure代码

(next stooges) ; -> ("Larry" "Curly" "Shemp")
(butlast stooges) ; -> ("Moe" "Larry" "Curly")
(drop-last 2 stooges) ; -> ("Moe" "Larry")
; Get names containing more than three characters.
(filter #(> (count %) 3) stooges) ; -> ("Larry" "Curly" "Shemp")
(nthnext stooges 2) ; -> ("Curly" "Shemp")

有很多断言函数来测试集合中每个值是否满足某种条件,并返回布尔结果。它们都是“短路运算”,只会对返回结果所必需的元素求值。举例如下:
Clojure代码

(every? #(instance? String %) stooges) ; -> true
(not-every? #(instance? String %) stooges) ; -> false
(some #(instance? Number %) stooges) ; -> nil
(not-any? #(instance? Number %) stooges) ; -> true

Lists

Lists是一种有序集合。当从开头添加新元素或者移除元素时(常数时间),list的表现是完美的。然而在根据索引获取某个元素时(采用nth)性能表现不佳(线性时间),而且没有高效的方式来通过索引改变集合元素。
下面是几种创建同样list的方式:
Clojure代码

(def stooges (list "Moe" "Larry" "Curly"))
(def stooges (quote ("Moe" "Larry" "Curly")))
(def stooges '("Moe" "Larry" "Curly"))

some函数

可以被用来确定某个集合是否包含指定的元素,它接受某个断言函数和某个集合作为参数。
或许定义某个断言函数来找出集合中是否存在某个指定元素确实是乏味的,实际上并不鼓励这种做法。
在list中查找某个确定元素是一种线性操作。采用set来代替list明显更高效且简单。
不管怎样,下面的例子展示了如何查找:
Clojure代码

(some #(= % "Moe") stooges) ; -> true
(some #(= % "Mark") stooges) ; -> nil
; Another approach is to create a set from the list
; and then use the contains? function on the set as follows.
(contains? (set stooges) "Moe") ; -> true

conj 和 cons函数 remove函数

conj 和 cons函数都可以将额外的元素添加到原有list上面去创建一个新的list。 remove函数创建一个新的list,里面仅包含原list中执行断言函数返回false结果的元素。例子如下:
Clojure代码

(def more-stooges (conj stooges "Shemp")) -> ("Shemp" "Moe" "Larry" "Curly")
(def less-stooges (remove #(= % "Curly") more-stooges)) ; -> ("Shemp" "Moe" "Larry")

into 函数

创建一个包含两个list所有元素的新list。例子如下:
Clojure代码

(def kids-of-mike '("Greg" "Peter" "Bobby"))
(def kids-of-carol '("Marcia" "Jan" "Cindy"))
(def brady-bunch (into kids-of-mike kids-of-carol))
(println brady-bunch) ; -> (Cindy Jan Marcia Greg Peter Bobby)

peek 和 pop 函数

能够将list当作栈来操作,它们都操作list的开始或者头元素。

Vectors

Vector也是有序集合。当从结尾处添加或者删除元素时(常数时间),vecor的表现很完美。这意味着采用conj函数来添加元素比 cons要更加高效。在vector中通过索引来检索(采用nth)或者改变(采用 assoc)元素的表现都很高效(常数时间)。函数定义时采用vector来作为参数列表。

这是一些创建vector的方法:
Clojure代码

(def stooges (vector "Moe" "Larry" "Curly"))
(def stooges ["Moe" "Larry" "Curly"])

除非特别需要从头部或者开始去添加或者移除元素才采用list之外,一般场合下,vector的表现都要优于list。这主要是由于vector的语法[…]要比list的语法 '(…)更加动人一点。它不会在函数、宏或者特殊form的调用中造成困扰。

get函数

在vector中根据索引检索出对应的元素。在之后的展示中,它同样在map中根据key检索出对应的值。
索引从0开始。get函数和nth函数很类似。它们俩都可以接受一个可选参数作为索引越界时的返回值。
如果没有这个参数,而索引越界了,get函数会返回nil,而nth函数会抛出一个异常。例子如下:
Clojure代码

(get stooges 1 "unknown") ; -> "Larry"
(get stooges 3 "unknown") ; -> "unknown"

assoc函数

操作vector和map。当它应用于vector时,会创建一个替换掉指定索引元素的新vector。
如果指定的索引正好等于vector的元素个数,则会把新元素添加到vector的尾部。
如果指定的索引大于vector元素的个数,则会抛出一个 IndexOutOfBoundsException。
例子如下:
Clojure代码

(assoc stooges 2 "Shemp") ; -> ["Moe" "Larry" "Shemp"]

subvec函数

会返回一个已存在vector的子集,其中的元素排序仍保留。它接受一个vector参数,
一个起始索引参数,一个可选的结束索引参数。如果结束索引参数被省略,
结果子集的末尾就是原来vector的末尾。新的结果vector同原来的vector共享结构。

之前所有作用于list的代码示例都同样可以作用于vector上,peek 和 pop 函数同样可以作用在vector上,但操作的是集合的尾部,而不像作用在list上操作的是集合的头部。 conj 函数会创建一个尾部添加了新元素的vector。 cons 函数则会创建一个头部添加了新元素的vector。

sets

set是元素唯一性的集合。当集合中元素不允许重复且不需要按次序添加时,比起list和vector来,set是较好的选择。Clojure支持两种类型的set,有序的和无序的。添加到有序set的元素不能彼此比较,否则会抛出一个ClassCastException。下面是创建set的数种方式:
Clojure代码

(def stooges (hash-set "Moe" "Larry" "Curly")) ; not sorted
(def stooges #{"Moe" "Larry" "Curly"}) ; same as previous
(def stooges (sorted-set "Moe" "Larry" "Curly"))

contains?函数

可以作用于set和map上面。当它作用于set上面时,用于确定set是否包含某特定元素。这比采用 some 函数在list或者vector检索元素要简单多了,见如下示例:
Clojure代码

(contains? stooges "Moe") ; -> true
(contains? stooges "Mark") ; -> false

set函数

能够当作函数使用,参数是它的元素,如果采用这种方式,它返回的是指定的元素或者nil。这提供了一个非常紧凑的方式来检阅集合中是否包含某指定元素。例子如下:
Clojure代码

(stooges "Moe") ; -> "Moe"
(stooges "Mark") ; -> nil
(println (if (stooges person) "stooge" "regular person"))

上面展示的作用于list的conj 和 into 函数同样可以作用于set。不过只有有序set才能定义添加新元素的位置。

disj 函数

创建一个新的set,内容是移除了当前set的一个或者多个元素。例子如下:
Clojure代码

(def more-stooges (conj stooges "Shemp")) ; -> #{"Moe" "Larry" "Curly" "Shemp"}
(def less-stooges (disj more-stooges "Curly")) ; -> #{"Moe" "Larry" "Shemp"}

clojure.set 命名空间

中包含了如下函数: difference , index , intersection , join , map-invert , project , rename , rename-keys , select 和 union。其中的某些函数是作用于map而不是set上的。

Maps

map存储key以及对应的值,其中key和值都能够是任何类型。通常map的key采用关键字,值则基于结对的key采用有序便于获取的方式存储。

下面是创建map的数种方式,以棒冰的颜色做为key,口味做为值,都采用关键字来存储它们之间的关系。采用逗号分隔可以帮助理解,这些逗号是可选的,在编译和运行时会被当作空白来处理。
Clojure代码

(def popsicle-map
(hash-map :red :cherry, :green :apple, :purple :grape))
(def popsicle-map
{:red :cherry, :green :apple, :purple :grape}) ; same as previous
(def popsicle-map
(sorted-map :red :cherry, :green :apple, :purple :grape))

map也可以当作函数使用,参数是其中的key。同样地,在某些情况下,key也可以当作函数使用,参数是其所属的map。但是,只有关键字类型的key可以这么使用,String和Integer 类型的key则不能。下面是几种有效的方式来获取绿色棒冰的口味,应该是apple:
Clojure代码

(get popsicle-map :green)
(popsicle-map :green)
(:green popsicle-map)

contains?函数可以作用于set和map

当它用于map时,它会检索map中是否存在对应的key。 keys 函数会返回指定map的所有key,并封装成一个序列。而 vals 函数则返回指定map的所有值序列。如下所示:
Clojure代码

(contains? popsicle-map :green) ; -> true
(keys popsicle-map) ; -> (:red :green :purple)
(vals popsicle-map) ; -> (:cherry :apple :grape)

assoc函数

可以作用于vector和map。
当它作用于map上时,它会创建一个新的map比原有map增加了任意数量的键值对。
如果新增的键值对在原有map中有已经存在的key,则对应的值会被替换成新的值。例子如下:
Clojure代码

(assoc popsicle-map :green :lime :blue :blueberry)
; -> {:blue :blueberry, :green :lime, :purple :grape, :red :cherry}

dissoc函数

第一个参数是map,后面可以跟任意数量map中的key做为参数。
它返回的是一个新的map,其中包含原map移除掉所有参数key对应键值对后剩下的元素。
如果指定的参数key在map中没有,则会被忽略掉,例子如下:
Clojure代码

(dissoc popsicle-map :green :blue) ; -> {:purple :grape, :red :cherry}

如果在序列的上下文中使用map,map会象clojure.lang.MapEntry序列对象一样来处理。可以采用 doseq 和 destructuring 函数来联合处理map中所有键值对的迭代,在下面的章节中会讲到。下面的例子遍历了popsicle-map,并将key绑定在 color 上面,值绑定在flavor上。然后用name函数返回了关键字的字符串名字。
Clojure代码

(doseq [[color flavor] popsicle-map]
(println (str "The flavor of " (name color)
" popsicles is " (name flavor) ".")))

上面代码的输出结果是:
Clojure代码

The flavor of green popsicles is apple.
The flavor of purple popsicles is grape.
The flavor of red popsicles is cherry.

select-keys函数

接收一个map以及一个key的序列做为参数,
返回一个新map包含原map中对应参数key序列的元素。
如果参数key序列中有不属于map的key,则不加理会。例子如下:
Clojure代码

(select-keys popsicle-map [:red :green :blue]) ; -> {:green :apple, :red :cherry}

conj函数

将一个map的所有键值对添加到另一个map中去,如果目标map中的key存在于添加的map中,
那么目标map中这个key的值会被覆盖掉。

map中的值也可以是map,并且可以嵌套任意层次。计算嵌套的值是非常容易的。同样地,改变map的嵌套值创建新map也很容易。

为了证明这一点,我们来创建一个描述人的map。它拥有一个address key,对应的值是一个描述地址的map。它还拥有一个employer key,对应的值当中包含一个address map。
Clojure代码

(def person {
:name "Mark Volkmann"
:address {
:street "644 Glen Summit"
:city "St. Charles"
:state "Missouri"
:zip 63304}
:employer {
:name "Object Computing, Inc."
:address {
:street "12140 Woodcrest Executive Drive, Suite 250"
:city "Creve Coeur"
:state "Missouri"
:zip 63141}}})

get-in函数

接收一个map参数和一个key序列参数,它返回的key序列中最后一个key对应的值,
而会把之前的key都当作嵌套的路径。而 ->宏和 reduce函数可以同样达到这个目的。
这些都在下面的例子中得到证明,我们将从person map中获取到 employer的city值--"Creve Coeur"。
Clojure代码

(get-in person [:employer :address :city])
(-> person :employer :address :city) ; explained below
(reduce get person [:employer :address :city]) ; explained below

-> 宏

被称为“thread”宏,调用了一系列的函数,将每个函数的结果做为参数传递给下个函数。
比如下面两条操作会得到一样的结果:
Clojure代码

(f1 (f2 (f3 x)))
(-> x f3 f2 f1)

-?> 宏

同样在命名空间clojure.contrib.core中存在 -?> 宏,它跟 -> 的区别在于
如果函数调用链中的某个函数返回nil,则中止调用并返回nil。
这样就避免了抛出一个 NullPointerException。

reduce 函数

接收三个参数,第一参数位是一个拥有两个参数的函数f;
第二参数位是一个值val,这个参数是可选的;第三参数位是一个集合coll。
如果val有值,那么val的值和coll的第一个元素被作为参数传给函数f;
如果val没有值,或者说没有val,那么coll的头两个元素被作为参数传给函数f。
这是函数f的第一次执行,这次执行的结果和coll中的下一个元素又会作为参数传给函数f,
这样一直执行下去,直到coll中所有的元素都被处理过。
这个函数类似Ruby中的inject,或者Haskell中的foldl。

assoc-in 函数

接收三个参数,一个map,一个key序列和一个新的值。
其中key序列代表map嵌套的路径,函数返回的是一个key序列中最后一个key的值被改变为新的值的map。
举个例子,我们根据person获得一个employer city改变为 "Clayton",而其他都不变的新map:
Clojure代码
(assoc-in person [:employer :address :city] "Clayton")

update-in 函数

接受一个map假设为m,一个key序列[k & ks],一个函数f以及任意数量的参数args。
毫无疑问,key序列仍然是表示map嵌套的路径。
然后,key序列中的最后一个元素对应的值以及参数args将被传给函数f,
f的返回值会作为key序列最后一个key所对应的新值。
update-in 函数返回的就是包含这个新值的新map。举个例子,
我们根据person获取一个 employer zip变为加4位格式的新map:
Clojure代码

(update-in person [:employer :address :zip] str "-1234") ; using the str function

StructMaps

StructMaps和正常的map很类似,不过它针对在多个实例间利用相同的key进行了优化,所以它不必重复。它们的用法有点类似Java Beans。在创建时会自动为StructMap生成合适的equals 和 hashCode方法。也能够轻松创建效率高于普通key索引得值的存取器函数。

create-struct 函数defstruct 宏

create-struct 函数,和 defstruct 宏(内部还是调用的 create-struct函数),都可以创建 StructMap。StructMap的key仍然是采用关键字。例子如下:
Clojure代码

(def car-struct (create-struct :make :model :year :color)) ; long way
(defstruct car-struct :make :model :year :color) ; short way

struct 函数

通过指定的 StructMap创建一个实例。创建实例时指定的值必须要和定义 StructMap时给定的key顺序一致且一一对应。如果后面的key没有对应的值,则它自动对应nil。例子如下:
Clojure代码

(def car (struct car-struct "Toyota" "Prius" 2009))

accessor 函数

创建一个在实例中根据key获取对应值的访问函数,以避免执行hashmap寻址。例子如下:
Clojure代码

; Note the use of def instead of defn because accessor returns
; a function that is then bound to "make".
(def make (accessor car-struct :make))
(make car) ; -> "Toyota"
(car :make) ; same but slower
(:make car) ; same but slower

在StructMap定义时没有指定的key可以添加给实例,但是,实例无法移除 StructMap定义时已经指定了的key。

序列(Sequences)

序列是集合的逻辑视图。很多东西都能够被处理为序列,包括Java集合,Clojure定制的集合,
字符串,流,文件结构和xml树。

延迟序列

很多Clojure函数返回一个延迟序列,这个序列中函数返回结果的元素除非在需要时,否则是不会被求值的。
采用延迟序列的好处在于,在序列创建时没必要去预期这个序列中会有多少元素被实际用到。
返回值是延迟序列的函数包括: cache-seq , concat , cycle , distinct , drop , drop-last , drop-while , filter , for , interleave , interpose , iterate , lazy-cat , lazy-seq , line-seq , map , partition , range , re-seq , remove ,
repeat , replicate , take , take-nth , take-while和 tree-seq。

延迟序列是新手Clojure开发者经常容易混淆的地方。举个例子,下面这段代码会输出什么?
Clojure代码

(map #(println %) [1 2 3])

当在REPL中运行时,先输出1,2,3,每个数字占一行,然后在同一行上输出3个nil,这是三次println函数调用
的返回值。REPL总是对输入的表达式完全求值。然而,当这段代码作为某个脚本中的一部分运行时,可能没有任何
输出。这是因为map会把第一个参数函数应用到第二个参数集合中,返回的结果组成一个延迟序列。map函数的文本
说明清晰地指出了它返回的是延迟序列。

有很多种方法让某些元素求值到延迟序列中。提取出一个单独元素的函数,比如:first , second , nth和 last都能
做到这一点。在序列中的元素会被依次求值,所以在被请求的元素之前的元素同样会被求值。举个例子,如果请求的
是最后一个元素,那么这个序列中所有的元素都会被求值。

如果一个延迟序列的头保持在某个绑定中,一旦其中某个元素被求值,它的值就会被缓存起来,因此,如果再次
请求这个元素,它就不会被重新求值。

dorun和 doall函数

会强制元素 求值到一个单一延迟序列中, doseq宏,早先在 "Iteration " 章节中讨论过,会强制
元素的求值到一个或者多个延迟序列中。for宏,在同样的章节中讨论过,并不强制求值,而是返回另一个延迟序列。

在执行时仅仅简单地需要一个副作用的情况下,采用doseq或者 dorun函数是合适的。执行的结果不会被保存,这样就能
节省相当的内存。这两个函数的返回值都是nil。而当返回结果需要保存时,采用doall函数就是合适的了,它保持了序列的
头让结果可以被缓存起来,并返回对序列的求值。

下面列表阐明了对延迟序列中的元素强制求值的选项:

保留执行结果 抛弃执行结果仅产生副作用 在单一序列上操作 采用列表包含语法操作任意数量的序列

doall dorun
N/A doseq

比起dorun来,优先采用doseq函数,因为代码的可读性要高些。它同样更快些,因为在dorun里面对map的调用会
创建一个新的序列。举个例子,下面两行代码会有一样的输出:
Clojure代码

(dorun (map #(println %) [1 2 3]))
(doseq [i [1 2 3]] (println i))

如果一个函数创建了一个延迟序列,其中每个元素在求值时都会产生副作用。在大多数场景下,应该采用doall函数
来强制求值其中的元素并返回结果。这样使得出现副作用的时机更加容易预测。否则调用者在多次对延迟序列求值时会
产生重复的副作用。

下面的表达式会在不同行上面都输出1, 2, 3,但是它们拥有不同的返回值。do特殊form应用于此来实现匿名函数中
执行多件事,打印出传入的参数以及返回值。
Clojure代码

(doseq [item [1 2 3]] (println item)) ; -> nil
(dorun (map #(println %) [1 2 3])) ; -> nil
(doall (map #(do (println %) %) [1 2 3])) ; -> (1 2 3)

延迟序列使得创建无限序列成为可能,因为在延迟序列中不必对每个拥有的元素求值。例子如下:
Clojure代码

(defn f  
  "square the argument and divide by 2"  
  [x]  
  (println "calculating f of" x)  
  (/ (* x x) 2.0))  
 
; Create an infinite sequence of results from the function f  
; for the values 0 through infinity.  
; Note that the head of this sequence is being held in the binding "f-seq".  
; This will cause the values of all evaluated items to be cached.  
(def f-seq (map f (iterate inc 0)))  
 
; Force evaluation of the first item in the infinite sequence, (f 0).  
(println "first is" (first f-seq)) ; -> 0.0  
 
; Force evaluation of the first three items in the infinite sequence.  
; Since the (f 0) has already been evaluated,  
; only (f 1) and (f 2) will be evaluated.  
(doall (take 3 f-seq))  
 
(println (nth f-seq 2)) ; uses cached result -> 2.0

然后我们对上面的程序做个改变,不在绑定中保持延迟序列的头。注意如何将已定义的序列做为函数的返回结果而不是
绑定值。这样就并未缓存集合中元素求值的结果,这样会降低内存占用,但是比起之前的程序来说,对集合中元素的请求
会更加低效。
Clojure代码

(defn f-seq [] (map f (iterate inc 0)))  
(println (first (f-seq))) ; evaluates (f 0), but doesn't cache result  
(println (nth (f-seq) 2)) ; evaluates (f 0), (f 1) and (f 2)

另外一种不在绑定中保持延迟序列头的做法是直接将序列传给一个会对其元素进行求值的函数。例子如下:
Clojure代码

(defn consumer [seq]  
  ; Since seq is a local binding, the evaluated items in it  
  ; are cached while in this function and then garbage collected.  
  (println (first seq)) ; evaluates (f 0)  
  (println (nth seq 2))) ; evaluates (f 1) and (f 2)  
 
(consumer (map f (iterate inc 0)))

Clojure-JVMλ语言(3) 函数定义和java交互

博客分类: 翻译Clojure
clojurelispjava
原帖地址:http://java.ociweb.com/mark/clojure/article.html#DefiningFunctions
作者: R. Mark Volkmann
译者: RoySong

函数定义

使用defn宏可以创建一个函数,它的参数是函数名,可选的函数说明(用doc可以查看这个说明),
参数列表(vector,可以为空),以及函数体。函数体中最后一个表达式的值做为函数的返回值。
每个函数都会返回一个值,虽然这个值有可能是nil。例子如下:
Clojure代码
(defn parting
"returns a String parting"
[name]
(str "Goodbye, " name)) ; concatenation

(println (parting "Mark")) ; -> Goodbye, Mark

函数必须在使用前先行定义,有时候由于一组函数相互调用而无法预先定义函数时,可以采用特殊form“declare”来预声明函数,declare一次可以声明多个没有具体实现的函数名。例子如下:
Clojure代码
(declare function-names)

采用defn-宏定义的函数是private的,这意味着它们仅能在被创建的命名空间中使用。其他定义private的宏,比如 defmacro- 和defstruct-,都在 clojure.contrib.def命名空间中。

函数可以接收一系列的参数,其中可选参数必须出现在参数列表的末尾,它们以名字前面加&的形式出现在参数列表的末尾。例子如下:
Clojure代码
(defn power [base & exponents]
; Using java.lang.Math static method pow.
(reduce #(Math/pow %1 %2) base exponents))
(power 2 3 4) ; 2 to the 3rd = 8; 8 to the 4th = 4096

函数可以拥有多个参数列表以及相应的函数体,每个参数列表必须包含不同的参数个数,这样就支持了基于参数的函数重载。通常在采用不同数量的参数调用同一个函数时为某些参数赋予初始值的场景下是非常有用的,例子如下:
Clojure代码
(defn parting
"returns a String parting in a given language"
([] (parting "World"))
([name] (parting name "en"))
([name language]
; condp is similar to a case statement in other languages.
; It is described in more detail later.
; It is used here to take different actions based on whether the
; parameter "language" is set to "en", "es" or something else.
(condp = language
"en" (str "Goodbye, " name)
"es" (str "Adios, " name)
(throw (IllegalArgumentException.
(str "unsupported language " language))))))

(println (parting)) ; -> Goodbye, World
(println (parting "Mark")) ; -> Goodbye, Mark
(println (parting "Mark" "es")) ; -> Adios, Mark
(println (parting "Mark", "xy"))
; -> java.lang.IllegalArgumentException: unsupported language xy

匿名函数没有名字,它们通常做为参数传递给一个实名函数。它们一般拥有一个简短的函数定义且仅用于一处。有两种方式定义匿名函数,如下:
Clojure代码
(def years [1940 1944 1961 1985 1987])
(filter (fn [year] (even? year)) years) ; long way w/ named arguments -> (1940 1944)
(filter #(even? %) years) ; short way where % refers to the argument

当采用fn定义匿名函数时,函数体可以包含任意数目的表达式。

当采用#(…)这样的缩略方式定义匿名函数时,函数体仅能包含一个表达式。
如果需要使用多个表达式,可以采用do将多个表达式包含起来。
如果仅有一个参数,可以采用%来指定它。
如果拥有多个参数,可以采用%1,%2这样累加的方式来指定。例子如下:
Clojure代码
(defn pair-test [test-fn n1 n2]
(if (test-fn n1 n2) "pass" "fail"))

; Use a test-fn that determines whether
; the sum of its two arguments is an even number.
(println (pair-test #(even? (+ %1 %2)) 3 5)) ; -> pass

java的方法可以基于参数类型重载,而Clojure的函数只能基于参数数量重载。不过,Clojure的多重方法(multimethods),可以基于任何东西重载。

联合采用defmulti和 defmethod宏可以定义多重方法。 defmulti的参数是方法名和一个分发函数,分发函数的返回值将用于选择某个方法。 defmethod的参数是方法名,触发方法使用的分发值,参数列表以及方法体。其中有一个特殊的分发值 :default用于方法调用时没有任何分发值符合定义的情况。每个拥有同样多重方法名的 defmethod定义必须拥有相同数量的参数。多重方法接收到这些参数后会传给分发函数。

下面是一个基于类型重载的多重方法例子:
Clojure代码
(defmulti what-am-i class) ; class is the dispatch function
(defmethod what-am-i Number [arg] (println arg "is a Number"))
(defmethod what-am-i String [arg] (println arg "is a String"))
(defmethod what-am-i :default [arg] (println arg "is something else"))
(what-am-i 19) ; -> 19 is a Number
(what-am-i "Hello") ; -> Hello is a String
(what-am-i true) ; -> true is something else

分发函数可以是任何函数,包含自定义函数,这个可能性是无止境的。举个例子,可以自定义一个分发函数检查参数并返回指定大小的关键字比如::small , :medium或者 :large。然后,每个关键字对应的方法可以根据具体的大小来执行业务逻辑。例子如下:
Clojure代码
user> (defn size-of [arg]
(cond
(> (count arg) 10) :large
(< (count arg) 10) :small
:else :medium))
user> (defmulti is-this-large size-of)
user> (defmethod is-this-large :large [arg] (println arg " is large"))
user> (defmethod is-this-large :medium [arg] (println arg " is medium"))
user> (defmethod is-this-large :small [arg] (println arg " is small"))
user> (is-this-large "1234567890") ;->1234567890 is medium
user> (is-this-large "12345678901") ;->12345678901 is large
user> (is-this-large "1234567") ;->1234567 is small

下划线可以用作函数参数的占位符,因为这个参数不会被使用,所以不需要名字。这通常用在被传给其他函数的回调函数(callback function)上面,某个特别的回调函数或许不会使用所有接收到的参数。举个例子:
Clojure代码
(defn callback1 [n1 n2 n3] (+ n1 n2 n3)) ; uses all three arguments
(defn callback2 [n1 _ n3] (+ n1 n3)) ; only uses 1st & 3rd arguments
(defn caller [callback value]
(callback (+ value 1) (+ value 2) (+ value 3)))
(caller callback1 10) ; 11 + 12 + 13 -> 36
(caller callback2 10) ; 11 + 13 -> 24

complement函数会根据已有函数生成一个新函数,但新函数的返回值跟原有函数的返回值逻辑相反。举个例子:
Clojure代码
(defn teenager? [age] (and (>= age 13) (< age 20)))
(def non-teen? (complement teenager?))
(println (non-teen? 47)) ; -> true

comp函数能够组合任意数量的函数成一个新函数,这些函数依照参数列表中从右向左的顺序依次调用。举个例子:
Clojure代码
(defn times2 [n] (* n 2))
(defn minus3 [n] (- n 3))
; Note the use of def instead of defn because comp returns
; a function that is then bound to "my-composition".
(def my-composition (comp minus3 times2))
(my-composition 4) ; 4*2 - 3 -> 5

partial函数根据已有函数生成一个新函数,这个新函数会为原函数的调用提供一个固定的初始化参数,
这叫做“局部应用”( partial application)。
举个例子,*是接收任意数量的参数,并返回它们相乘的结果。
假设我们需要一个新版本的乘法函数,需要把每次相乘的结果再乘以2:
Clojure代码
; Note the use of def instead of defn because partial returns
; a function that is then bound to "times2".
(def times2 (partial * 2))
(times2 3 4) ; 2 * 3 * 4 -> 24

下面是一些采用map和partial函数的有趣应用。我们将定义一个函数,它能够计算任意多项式,
根据传入的x值获得对应的导数。多项式通过vector装载系数来表示。
然后,我们定义函数并采用 partial来定义指定的多项式和它的导数。最后,我们采用这些函数来求值:
Clojure代码
(defn- polynomial
"computes the value of a polynomial
with the given coefficients for a given value x"
[coefs x]
; For example, if coefs contains 3 values then exponents is (2 1 0).
(let [exponents (reverse (range (count coefs)))]
; Multiply each coefficient by x raised to the corresponding exponent
; and sum those results.
; coefs go into %1 and exponents go into %2.
(apply + (map #(* %1 (Math/pow x %2)) coefs exponents))))

(defn- derivative
"computes the value of the derivative of a polynomial
with the given coefficients for a given value x"
[coefs x]
; The coefficients of the derivative function are obtained by
; multiplying all but the last coefficient by its corresponding exponent.
; The extra exponent will be ignored.
(let [exponents (reverse (range (count coefs)))
derivative-coefs (map #(* %1 %2) (butlast coefs) exponents)]
(polynomial derivative-coefs x)))

(def f (partial polynomial [2 1 3])) ; 2x^2 + x + 3
(def f-prime (partial derivative [2 1 3])) ; 4x + 1

(println "f(2) =" (f 2)) ; -> 13.0
(println "f'(2) =" (f-prime 2)) ; -> 9.0

还有另一种方法来实现多项式函数(Francesco Strino提出的建议)。
对于一个拥有系数a,b,c的多项式,可以这样根据x来计算值:
%1 = a, %2 = b, result is ax + b
%1 = ax + b, %2 = c, result is (ax + b)x + c = ax^2 + bx + c
Clojure代码
(defn- polynomial
"computes the value of a polynomial
with the given coefficients for a given value x"
[coefs x]
(reduce #(+ (* x %1) %2) coefs))

memoize函数接收一个函数做为参数,并返回一个以映射形式储存了原函数参数以及对应返回值的新函数。
新函数采用映射可以在以相同参数调用函数时不必重复计算。这样能获得更好的性能,
但会占用更多的内存来储存映射关系。

time宏对一个表达式求值,打印出表达式的执行时间,并返回执行的结果。接下来会使用它来对多项式的执行计时。

下面的例子展示了缓存多项式函数:
Clojure代码
; Note the use of def instead of defn because memoize returns
; a function that is then bound to "memo-f".
(def memo-f (memoize f))

(println "priming call")
(time (f 2))

(println "without memoization")
; Note the use of an underscore for the binding that isn't used.
(dotimes [_ 3] (time (f 2)))

(println "with memoization")
(dotimes [_ 3] (time (memo-f 2)))

上面的代码执行结果如下:
Clojure代码
priming call
"Elapsed time: 4.128 msecs"
without memoization
"Elapsed time: 0.172 msecs"
"Elapsed time: 0.365 msecs"
"Elapsed time: 0.19 msecs"
with memoization
"Elapsed time: 0.241 msecs"
"Elapsed time: 0.033 msecs"
"Elapsed time: 0.019 msecs"

这个输出提供了很多观察结果,第一次对函数f的调用,“主调用”(priming call),比起其他调用来花费了相当长的时间。这时完全不管是否使用了缓存。第一次调用缓存方法比第一次对“非主调用”(non-priming call)花费的时间要长,因为缓存结果付出了开支。随后的缓存调用就要快得多了。

Java 交互

Clojure程序可以调用所有的java类和接口,如同在java中一样,调用java.lang包下面的东西是不需要引入的。如果需要使用其他包中的java类或者接口,只需要指定对应的包或者采用 import函数来引入对应的类或者接口就可以了。示例如下:
Clojure代码
(import
'(java.util Calendar GregorianCalendar)
'(javax.swing JFrame JLabel))

同样可以参考ns宏中的
:import
指令,在之后的章节中会进行描述。

有两种方式来使用java类中的常量,示例如下:
Clojure代码
(. java.util.Calendar APRIL) ; -> 3
(. Calendar APRIL) ; works if the Calendar class was imported
java.util.Calendar/APRIL
Calendar/APRIL ; works if the Calendar class was imported

在Clojure代码中调用java方法同样非常简单。因为这个缘故,Clojure并没有提供很多公用方法而是直接依赖于java方法。举个例子,Clojure并不提供对浮点数取绝对值的函数,因为java.lang.Math类中的abs方法已经提供了相同的功能。而另一方面, java.lang.Math类中的 max方法只提供对两个数取最大的功能,所以Clojure提供了可以获取多个数中最大数的max函数。

有两种方式来调用java类中的静态方法,示例如下:
Clojure代码
(. Math pow 2 4) ; -> 16.0
(Math/pow 2 4)

有两种方式能调用构造函数来生成一个java对象,稍后有示例。
注意采用了def来对新生成的对象的引用保持了一个全局绑定,这并不是必要的。
这个引用可以用多种方式来保持,比如将它添加到某个集合中,或者将它传递给某个函数。
Clojure代码
(import '(java.util Calendar GregorianCalendar))
(def calendar (new GregorianCalendar 2008 Calendar/APRIL 16)) ; April 16, 2008
(def calendar (GregorianCalendar. 2008 Calendar/APRIL 16))

有两种方式来调用java对象的实例方法,示例如下:
Clojure代码
(. calendar add Calendar/MONTH 2)
(. calendar get Calendar/MONTH) ; -> 5
(.add calendar Calendar/MONTH 2)
(.get calendar Calendar/MONTH) ; -> 7

上面的例子中,通常建议优先采用方法名在第一位的调用方式。
而对象在第一位的调用方式通常在宏定义中使用,因为句点引用可以用来替换字符串连接。
在阅读过"Macros "章节后,这一段的意义会更加明确。

可以采用宏来进行链式方法调用,能够将前一个方法调用的结果做为后一个方法调用的主体,示例如下:
Clojure代码
(. (. calendar getTimeZone) getDisplayName) ; long way
(.. calendar getTimeZone getDisplayName) ; -> "Central Standard Time"

同样,在clojure.contrib.core命名空间中的 .?.宏在链式调用方法时遇到任何方法返回null就能够中止调用并返回nil,这样就避免抛出 NullPointerException。

doto函数用来调用同一对象中的多个方法,它返回的是第一个参数即被调用的目标对象。
这样就可以很方便地采用表达式来创建目标对象(参见在 "Namespaces "章节中创建JFrame GUI对象)。
示例如下:
Clojure代码
(doto calendar
(.set Calendar/YEAR 1981)
(.set Calendar/MONTH Calendar/AUGUST)
(.set Calendar/DATE 1))
(def formatter (java.text.DateFormat/getDateInstance))
(.format formatter (.getTime calendar)) ; -> "Aug 1, 1981"

memfn宏能够扩展代码运行java方法做为第一类函数处理,这是采用匿名函数来调用java方法的替代方案。
当采用 memfn来调用拥有参数的java方法时,每个参数必须指定参数名。 这指明了方法被调用时的参数数量。
这些参数名可以任意指定,但必须保障它们都是独特的,因为在生成的代码中会采用这些名字。
下面的例子对第一个集合中的java对象(String)调用了substring实例方法,
将第二个集合中对应的元素(int)传递给了方法做为参数。
Clojure代码
(println (map #(.substring %1 %2)
["Moe" "Larry" "Curly"] [1 2 3])) ; -> (oe rry ly)

(println (map (memfn substring beginIndex)
["Moe" "Larry" "Curly"] [1 2 3])) ; -> same
代理
proxy函数创建了一个继承指定java类以及(或者)实现了零个或者一到多个java接口的java对象。
这通常需要在监听对象上实现回调方法,这个监听对象必须实现一个指定的接口以便从其他对象处获取通知。
举个例子,参见本文结束处的 "Desktop Applications "章节,其中的对象继承了JFrame GUI类并实现了ActionListener接口。

线程
所有的Clojure函数都实现了
java.lang.Runnable
接口和
java.util.concurrent.Callable
接口。
这使得它们能够很轻松地在新的java线程中执行。实例如下:
Clojure代码
(defn delayed-print [ms text]
(Thread/sleep ms)
(println text))

; Pass an anonymous function that invokes delayed-print
; to the Thread constructor so the delayed-print function
; executes inside the Thread instead of
; while the Thread object is being created.
(.start (Thread. #(delayed-print 1000 ", World!"))) ; prints 2nd
(print "Hello") ; prints 1st
; output is "Hello, World!"
异常处理
Clojure代码抛出的所有异常都是运行时异常(runtime exceptions)。Clojure代码中调用的java方法抛出的异常仍然是已检查异常(checked exceptions)。而Clojure中的特殊form:try,catch,finally,throw,在功能上和java当中的版本非常类似。示例如下:
Clojure代码
(defn collection? [obj]
(println "obj is a" (class obj))
; Clojure collections implement clojure.lang.IPersistentCollection.
(or (coll? obj) ; Clojure collection?
(instance? java.util.Collection obj))) ; Java collection?

(defn average [coll]
(when-not (collection? coll)
(throw (IllegalArgumentException. "expected a collection")))
(when (empty? coll)
(throw (IllegalArgumentException. "collection is empty")))
; Apply the + function to all the items in coll,
; then divide by the number of items in it.
(let [sum (apply + coll)]
(/ sum (count coll))))

(try
(println "list average =" (average '(2 3))) ; result is a clojure.lang.Ratio object
(println "vector average =" (average [2 3])) ; same
(println "set average =" (average #{2 3})) ; same
(let [al (java.util.ArrayList.)]
(doto al (.add 2) (.add 3))
(println "ArrayList average =" (average al))) ; same
(println "string average =" (average "1 2 3 4")) ; illegal argument
(catch IllegalArgumentException e
(println e)
;(.printStackTrace e) ; if a stack trace is desired
)
(finally
(println "in finally")))

上面代码产生的输出如下:
Clojure代码
obj is a clojure.lang.PersistentList
list average = 5/2
obj is a clojure.lang.LazilyPersistentVector
vector average = 5/2
obj is a clojure.lang.PersistentHashSet
set average = 5/2
obj is a java.util.ArrayList
ArrayList average = 5/2
obj is a java.lang.String
#<IllegalArgumentException java.lang.IllegalArgumentException:
expected a collection>
in finally

Clojure-JVMλ语言(4) 程序流控制

博客分类: 翻译Clojure
clojurejavalispjvm
原帖地址:http://java.ociweb.com/mark/clojure/article.html#ConditionalProcessing
作者:R. Mark Volkmann
译者:RoySong

程序流控制

条件判断
特殊form if会检验一个条件,然后根据检验结果来决定执行两个表达式中的哪一个。
它的语法是(if condition
then-expr
else-expr
),其中的else部分( else-expr
)是可选的。
如果then部分或者else部分需要不止一个表达式,则采用特殊form do来将它们包装成一个表达式。
例子如下:
Clojure代码
(import '(java.util Calendar GregorianCalendar))
(let [gc (GregorianCalendar.)
day-of-week (.get gc Calendar/DAY_OF_WEEK)
is-weekend (or (= day-of-week Calendar/SATURDAY) (= day-of-week Calendar/SUNDAY))]
(if is-weekend
(println "play")
(do (println "work")
(println "sleep"))))

when和 when-not在只需要一种分支的情况下可以替代if,它们的方法体中可以包含任意数目的表达式,而不用do来包装。
例子如下:
Clojure代码
(when is-weekend (println "play"))
(when-not is-weekend (println "work") (println "sleep"))

if-let宏将一个值绑定到某个符号上,然后根据这个值的逻辑真或者假(在 "Predicates "这一节中解释)
来决定执行哪个表达式。下面的代码会打印出在队列中等待的第一个用户的名字,如果队列中没有等待的用户,
则打印出"no waiting"。
Clojure代码
(defn process-next [waiting-line]
(if-let [name (first waiting-line)]
(println name "is next")
(println "no waiting")))

(process-next '("Jeremy" "Amanda" "Tami")) ; -> Jeremy is next
(process-next '()) ; -> no waiting

when-let宏跟if-let宏很类似,它们的不同和if跟when的不同方式一样。
when-let不支持else部分,并且when部分可以包含任意数目的表达式。例子如下:
Clojure代码
(defn summarize
"prints the first item in a collection
followed by a period for each remaining item"
[coll]
; Execute the when-let body only if the collection isn't empty.
(when-let [head (first coll)]
(print head)
; Below, dec subtracts one (decrements) from
; the number of items in the collection.
(dotimes [_ (dec (count coll))] (print \.))
(println)))

(summarize ["Moe" "Larry" "Curly"]) ; -> Moe..
(summarize []) ; -> no output

condp宏跟其他编程语言的case声明类似。它的参数包含一个双参数断言(通常采用=或者 instance? ),
一个表达式作为第二个参数,后面紧跟任意数量成对的值/结果表达式。这些成对值/结果中的值会被依次传入
断言中,如果断言执行结果为真,则返回对应表达式执行的结果;如果断言执行结果为假,则继续下一个
值/结果的断言验证。最后还可以定义一个可选的参数,在如果所有的值/结果断言验证都为假的情况下,
来返回一个值。如果没定义这个可选参数,而所有的断言验证都为假,就会抛出一个
IllegalArgumentException 。

下面的例子展示了如果用户输入数字1,2,3时就会打印出对应的名字,其他情况,则会打印出
"unexpected value"。在这之后,会检查本地绑定"value"的类型,如果是number类型,就会打印出这个
数字的2倍值;如果是String类型,则会打印出这个字符串长度的2倍值。
Clojure代码
(print "Enter a number: ") (flush) ; stays in a buffer otherwise
(let [reader (java.io.BufferedReader. *in*) ; stdin
line (.readLine reader)
value (try
(Integer/parseInt line)
(catch NumberFormatException e line))] ; use string value if not integer
(println
(condp = value
1 "one"
2 "two"
3 "three"
(str "unexpected value, \"" value \")))
(println
(condp instance? value
Number (* value 2)
String (* (count value) 2))))

cond宏接收任意数量的成对断言/结果表达式,它会依次对每个断言求值,直到某个断言的值为真,
则返回断言对应的结果。如果没有任何断言的值为真,则会抛出 IllegalArgumentException 。
通常最后一个断言会简单地写为true,来对应生下的所有情况。

下面的例子是由用户输入水温,程序会打印出水是冰的、烫的或者其他。
Clojure代码
(print "Enter water temperature in Celsius: ") (flush)
(let [reader (java.io.BufferedReader. *in*)
line (.readLine reader)
temperature (try
(Float/parseFloat line)
(catch NumberFormatException e line))] ; use string value if not float
(println
(cond
(instance? String temperature) "invalid temperature"
(<= temperature 0) "freezing"
(>= temperature 100) "boiling"
true "neither")))
迭代
有很多种方式来进行“循环”或者对集合中的元素进行迭代。

dotimes宏会将方法体中的表达式重复执行指定的次数,指定给本地绑定的执行序列值从0开始一直到次数减一。
如果这个本地绑定(比如下例中的card-number )是不需要的,则可以采用下划线做为占位符。例子如下:
Clojure代码
(dotimes [card-number 3]
(println "deal card number" (inc card-number))) ; adds one to card-number

注意采用了inc函数,所以输出值由0,1,2变成了1,2,3.上面代码的输出如下:
Clojure代码
deal card number 1
deal card number 2
deal card number 3

while宏会在它的判断表达式( test expression)为true时一直执行方法体中的表达式。
下面的例子会在指定线程保持运行的过程中一直执行while主体中的内容:
Clojure代码
(defn my-fn [ms]
(println "entered my-fn")
(Thread/sleep ms)
(println "leaving my-fn"))

(let [thread (Thread. #(my-fn 1))]
(.start thread)
(println "started thread")
(while (.isAlive thread)
(print ".")
(flush))
(println "thread stopped"))

这段程序的输入大致如下:
Clojure代码
started thread
…..entered my-fn.
………….leaving my-fn.
thread stopped
列表解析
for和doseq宏能够执行列表解析。它们支持遍历多重集合(最右的集合最快),还可以选择:when和:while表达式来进行过滤。for宏以一个单独的表达式做为执行主体,返回一个延迟序列结果。 doseq宏以一系列任意数量的表达式做为主体,依次执行以获得它们的副作用,然后返回nil。

下面的例子是遍历一张表格打印出遍历到的单元格,先逐行遍历,在每行中又逐列遍历。中间跳过了B这一列且只遍历小于3的行。注意其中的dorun宏,被用来强制对for返回的延迟序列求值,其详细用法将在"Sequences "这一节描述。
Clojure代码
(def cols "ABCD")
(def rows (range 1 4)) ; purposely larger than needed to demonstrate :while

(println "for demo")
(dorun
(for [col cols :when (not= col \B)
row rows :while (< row 3)]
(println (str col row))))

(println "\ndoseq demo")
(doseq [col cols :when (not= col \B)
row rows :while (< row 3)]
(println (str col row)))

上面代码产生以下结果:
Clojure代码
for demo
A1
A2
C1
C2
D1
D2

doseq demo
A1
A2
C1
C2
D1
D2

特殊form loop,就像名字所展示的那样,支持循环(loop)。
它以及和它合作的特殊form recur都将在下节中进行说明。

递归
递归是指当一个函数直接或者间接通过其他函数调用自身的情况。
递归中止的条件通常是某个集合元素变空或者某个数字已经达到了指定的值。
前一种情况通常是持续采用next函数不断返回除去了头元素的集合来实现,
而后一种情况则通常是采用dec函数 不断对某个数字持续相减来实现的。

在递归时,如果调用栈层次太深,有可能出现运行时内存溢出的情况。
某些程序语言会采用"tail call optimization " (TCO)的方式来处理这个问题,
java不支持,但是Clojure支持。
在Clojure中避免内存溢出的一种方式是采用loop和recur特殊form,
另外一种是采用 trampoline 函数。

loop /recur的约定组合看起来象是在循环中调用递归,但并不消费栈空间。
特殊form loop和特殊form let相似的地方就在于,它们都是建立本地绑定,
但loop同时会建立一个递归节点以供recur来进行调用。loop在创建本地绑定
时会为它指定一个初始值。接下来的recur调用完成后会将控制权交还给loop,
并为本地绑定指定一个新的值。被传递给recur的参数数量必须和loop建立的
绑定数量相同,同样,recur只能出现在loop调用的结尾处。
Clojure代码
(defn factorial-1 [number]
"computes the factorial of a positive integer
in a way that doesn't consume stack space"
(loop [n number factorial 1]
(if (zero? n)
factorial
(recur (dec n) (* factorial n)))))

(println (time (factorial-1 5))) ; -> "Elapsed time: 0.071 msecs"\n120

defn宏,就跟loop特殊form一样,也建立了一个递归点。
recur特殊form也可以放到函数的末尾处来为函数传递一个新的值来进行递归调用。

实现阶乘的另外一种方法是采用reduce函数,它在之前的 "Collections "章节已经描述过了。
它看起来更加函数化,更少指令式编程的样式。不幸的是,在当前例子中,它会更低效。
注意range函数接收了一个包含的低层绑定( lower bound)和一个未包含的高层绑定( upper bound)。
Clojure代码
(defn factorial-2 [number] (reduce * (range 2 (inc number))))

(println (time (factorial-2 5))) ; -> "Elapsed time: 0.335 msecs"\n120

采用apply替换掉reduce也能获得相同的结果,但那会花销更长的时间。
这说明了在选择函数时了解它们特征的重要性。

recur特殊form并不适用于a函数调用b函数,b函数再调用a函数这种 相互递归 的情况。
而这篇文章没提到的
trampoline
函数更适合相互递归一些。

条件判断
Clojure提供了很多函数来检验某个条件,执行判断。它们的返回值都可以解读为true或者false。
false和nil值都会被解读为false。true和其他任何值,包含0,都会被解读为true。
条件判断函数通常采用?结尾。

反射调用包含了对象的信息,不仅有值,还包括类型等。有很多条件判断函数都是执行的反射。
检验单一对象类型的判断函数有:class? , coll? , decimal? , delay? , float? , fn? , instance? ,
integer? , isa? , keyword? , list? , macro? , map? , number? , seq? , set? , string?和 vector? 。
而某些执行了反射的非判断函数有: ancestors , bases , class , ns-publics和 parents。

检验值之间关系的判断函数有: < , <= , = , not= , == , > , >= , compare , distinct?和 identical?。

检验逻辑关系的判断函数有:and , or , not , true? , false?和 nil?。

检验序列的判断函数大部分在之前我们都遇到过了,包括:empty? , not-empty , every? ,
not-every? , some和 not-any?。

检验数字的判断函数有: even? , neg? , odd? , pos?和 zero?。

Clojure-JVMλ语言(5) Input/Output

博客分类: 翻译Clojure
clojurejavalispjvm
原帖地址:http://java.ociweb.com/mark/clojure/article.html#IO
作者:R. Mark Volkmann
译者:RoySong

Input/Output

Clojure提供了针对I/O操作的最小限度函数集合。因为在Clojure代码可以轻松调用java代码,所以针对I/O操作
经常使用的是java.io包中的类。然而,在 Clojure Contrib 中的duck-streams库使得对java io类库的调用更简单。

预定义的特殊符号*in* , *out*和 *err*默认提供了标准输入、输出和错误的功能。在 *out*中flush流输出可以采用
(flush),这等同于 (.flush *out*)。这些特殊符号的绑定是可以修改的。举个例子,将默认输出重定向为往文件“
my.log”中:
Clojure代码
(binding [*out* (java.io.FileWriter. "my.log")]

(println "This goes to the file my.log.")

(flush))

print函数可以输出以空格分隔的 任意数目 对象的字符串表现到 特殊符号*out*的流中。

println函数类似print,不同点在于它会在每次输出的后面加上换行符。默认情况下,它会刷新输出。这个可以通过
指定特殊符号*flush-on-newline*为false来改变。

newline函数会为*out*中的流添加上换行符,在print后面跟上 newline的执行结果等同于println。

pr和 prn函数和print以及println很相似,不过它们的输出在一个form当中,这个form能够被Clojure reader读取。它们
适合序列化Clojure的数据结构。默认情况下,它们不会打印元数据。这个可以通过绑定特殊符号*print-meta*为true来改变。

下面的例子展示了四种打印函数,注意在采用pr和print时打印字符串和字符的差异:
Clojure代码
(let [obj1 "foo"
obj2 {:letter \a :number (Math/PI)}] ; a map
(println "Output from print:")
(print obj1 obj2)

(println "Output from println:")
(println obj1 obj2)

(println "Output from pr:")
(pr obj1 obj2)

(println "Output from prn:")
(prn obj1 obj2))

上面的代码输出如下:
Clojure代码
Output from print:
foo {:letter a, :number 3.141592653589793}Output from println:
foo {:letter a, :number 3.141592653589793}
Output from pr:
"foo" {:letter \a, :number 3.141592653589793}Output from prn:
"foo" {:letter \a, :number 3.141592653589793}

上面提到的所有函数都在输出的参数之间有个空格,采用str函数可以避免这个空格。它连接了所有输出参数的字符串
表现,例子如下:
Clojure代码
(println "foo" 19) ; -> foo 19
(println (str "foo" 19)) ; -> foo19

print-str , println-str , pr-str 和prn-str同 print , println , pr和 prn很相似,但是输出的目标从 *out*变成了一个字符串,这个字符串也做为它们的返回值。

with-out-str宏捕获它内部所有表达式的输出并把这些输出放置在一个字符串中,然后将这个字符串做为返回值。

with-open宏接受任意数量的对象绑定,在它内部的表达式执行完毕后会调用对象的 .close方法。这是为了处理需要
关闭的资源诸如文件或者数据库连接而设定的。

line-seq函数接受一个 java.io.BufferedReader做为参数,并返回一个延迟序列,序列中包含了参数中读取到的所有
文本行。返回“延迟”序列的意义在于,在序列被调用时不会读取到所有的文本行, 这样就不会消耗太多内存。每次请求
延迟序列时,只会读取对应的一行。

下面的例子展示了with-open 和line-seq的使用,它会读取某文件中的所有行,并输出包含某个特定字符的行。它采用了
两种方式,先是with-open,然后是 line-seq,它们都包含在 Clojure Contrib 的duck-streams库中。
Clojure代码
(use '[clojure.contrib.duck-streams :only (read-lines)])

(defn print-if-contains [line word]
(when (.contains line word) (println line)))

(let [file "story.txt"
word "fur"]

; with-open will close the FileReader and BufferedReader
; after evaluating all the expressions in its body.
(with-open [fr (java.io.FileReader. file)
br (java.io.BufferedReader. fr)]
(doseq [line (line-seq br)] (print-if-contains line word)))

; read-lines closes the Reader it creates
; after all the lines it returns in a lazy sequence are consumed.
(doseq [line (read-lines file)] (print-if-contains line word)))

slurp函数读取文件的整个文本并放置在返回结果的字符串中, duck-streams库提供了spit函数来将字符串写入到
文件中并关闭文件。

这篇文章仅仅涉及到duck-streams库的表层而已,去阅读duck-streams.clj文件学习其中定义的函数是更好的选择。

Clojure-JVMλ语言(6) 可变性

博客分类: 翻译Clojure
clojurejavalispjvm
原帖地址:http://java.ociweb.com/mark/clojure/article.html#Destructuring
作者:R. Mark Volkmann
译者:RoySong

可变性(Destructuring)

可变性可以用于宏或者函数的参数列表中来提取参数集合的部分进行本地绑定。它同样可以用在let特殊form
或者binding宏中来创建本地绑定。

举个例子,假设我们需要一个函数接受一个list或者vector做为参数,返回参数集合的第一和第三个元素相加
的值:
Clojure代码
(defn approach1 [numbers]
(let [n1 (first numbers)
n3 (nth numbers 2)]
(+ n1 n3)))

; Note the underscore used to represent the
; second item in the collection which isn't used.
(defn approach2 [[n1 _ n3]] (+ n1 n3))

(approach1 [4 5 6 7]) ; -> 10
(approach2 [4 5 6 7]) ; -> 10

&号也能够结合可变性使用来获取一个集合剩余的部分。例子如下:
Clojure代码
(defn name-summary [[name1 name2 & others]]
(println (str name1 ", " name2) "and" (count others) "others"))

(name-summary ["Moe" "Larry" "Curly" "Shemp"]) ; -> Moe, Larry and 2 others

:as关键字能够被用来对具备可变性的集合保持整体访问权限。假设我们需要一个函数接受一个list或者
vector做为参数,返回值是第一个元素加上第三个元素然后再除以所有元素相加的和的结果:
Clojure代码
(defn first-and-third-percentage [[n1 _ n3 :as coll]]
(/ (+ n1 n3) (apply + coll)))

(first-and-third-percentage [4 5 6 7]) ; ratio reduced from 10/22 -> 5/11

可变性同样可以应用在从map中提取值上面,假设我们需要一个函数接受一个销售价格map参数,
map的key是月份,对应的value是这个月的总销售额。这个函数将夏季月份(6,7,8月)的销售额
相加,再除以整个年度的销售额以获取夏季月份的销售额百分比:
Clojure代码
(defn summer-sales-percentage
; The keywords below indicate the keys whose values
; should be extracted by destructuring.
; The non-keywords are the local bindings
; into which the values are placed.
[{june :june july :july august :august :as all}]
(let [summer-sales (+ june july august)
all-sales (apply + (vals all))]
(/ summer-sales all-sales)))

(def sales {
:january 100 :february 200 :march 0 :april 300
:may 200 :june 100 :july 400 :august 500
:september 200 :october 300 :november 400 :december 600})

(summer-sales-percentage sales) ; ratio reduced from 1000/3300 -> 10/33

在对map实施可变性时,本地绑定的名字和对应key的名字相同是一种常用做法。比如,在上面的例子中我们
采用了{june :june july :july august :august :as all}。这实际上可以被简化为 :keys。比如:
{:keys [june july august] :as all}。

Clojure-JVMλ语言(7) 命名空间

博客分类: 翻译Clojure
clojurejavalispjvm
原帖地址:http://java.ociweb.com/mark/clojure/article.html#Namespaces
作者:R. Mark Volkmann
译者:RoySong

命名空间(Namespaces)

java通过package来对类以及其中的方法来分组,而Clojure采用命名空间来对通过符号命名的东西来分组。能够
进行分组的东西包含:Vars, Refs, Atoms, Agents, functions, macros以及命名空间本身。

符号被用来指定函数、宏和绑定的名称。而符号本身的作用域取决于它所属的命名空间。在Clojure中,总是会存在
一个默认的命名空间,最初是"user",它被保存在特殊符号*ns*中。默认命名空间可以采用两种方式来改变, in-ns函数
仅仅改变默认命名空间,而ns宏除了改变命名空间之外还有其他功能。其中一项功能是能够使 clojure.core命名空间中
包含的符号可以在新的命名空间中生效(采用refer,接下来会讨论到 )。ns宏的其他特性将过会儿讨论。

"user"命名空间提供了对所有clojure.core命名空间中符号访问的权限。采用ns宏改变的默认命名空间同样也具备这
样的权限。

为了使用不在默认命名空间中的元素必须进行命名空间限定(namespace-qualified),在符号名前面加上命名空间
的名字和斜杠就完成了限定。举个例子, Clojure Contrib 中的str-utils库在clojure.contrib.str-utils命名空间中定
义了 str-join函数。它取出某个序列中所有元素的字符串表现,并通过指定的连接符将它们连接成一个新的字符串做为
返回值。它的命名空间限定名就是clojure.contrib.str-utils/str-join。

require函数用于加载Clojure库。它接受一个或者多个带引号的命名空间名做为参数。例子如下:
Clojure代码
(require 'clojure.contrib.str-utils)

但这仅仅是加载了库,要使用库中的名字仍然需要采用命名空间限定。注意命名空间名和符号名采用斜杠分隔,而java
的包名和类名之间采用点来分隔。举个例子:
Clojure代码
(clojure.contrib.str-utils/str-join "$" [1 2 3]) ; -> "1$2$3"

alias函数创建一个命名空间的引用,以免每次都要输入长长的命名空间限定。所创建引用的作用域即当前命名空间。
例子如下:
Clojure代码
(alias 'su 'clojure.contrib.str-utils)
(su/str-join "$" [1 2 3]) ; -> "1$2$3"

refer函数可以让指定命名空间的所有符号在当前命名空间可用,而不用做命名空间限定。如果被指定的命名空间已经
在当前命名空间中定义,则会抛出一个异常。例子如下:
Clojure代码
(refer 'clojure.contrib.str-utils)

采用了refer后之前的代码就可以写成这种形式了:
Clojure代码
(str-join "$" [1 2 3]) ; -> "1$2$3"

require 和refer通常联合使用,所以有一个快捷函数use提供来实现它们联合的功能:
Clojure代码
(use 'clojure.contrib.str-utils)

ns宏,之前提到过,用于改变默认命名空间。它通常用在源文件的开头。它支持以下指令::require , :use 和:import
(用于引入java类),用于替代这些指令对应的函数形式。优先采用这些指令而不是它们对应的函数形式。在下面的例子
当中,注意采用:as来创建了一个命名空间的引用,同样也要注意采用了:only来加载Clojure库的部分:
Clojure代码
(ns com.ociweb.demo
(:require [clojure.contrib.str-utils :as su])
(:use [clojure.contrib.math :only (gcd, sqrt)])
(:import (java.text NumberFormat) (javax.swing JFrame JLabel)))

(println (su/str-join "$" [1 2 3])) ; -> 1$2$3
(println (gcd 27 72)) ; -> 9
(println (sqrt 5)) ; -> 2.236
(println (.format (NumberFormat/getInstance) Math/PI)) ; -> 3.142

; See the screenshot that follows this code.
(doto (JFrame. "Hello")
(.add (JLabel. "Hello, World!"))
(.pack)
(.setDefaultCloseOperation JFrame/EXIT_ON_CLOSE)
(.setVisible true))

create-ns创建了一个新的命名空间,但并没将它设置为默认命名空间。 def函数在默认命名空间中采用可选的初始值
定义了一个符号; intern函数在指定的命名空间中采用可选的初始值定义了一个符号(前提是这个命名空间中还没有相
同的符号)。注意一点,采用 intern函数时需要在符号名前加上单引号,def则不用。因为def是个特殊form,并不会对
它所有的参数求值,而intern是个函数,这代表这它会对所有的参数进行求值,例子如下:
Clojure代码
(def foo 1)
(create-ns 'com.ociweb.demo)
(intern 'com.ociweb.demo 'foo 2)
(println (+ foo com.ociweb.demo/foo)) ; -> 3

ns-interns函数返回一个包含指定当前已加载命名空间中所有符号的map,这个map的key是符号名,value是Var对象
可能代表函数、宏或者绑定。例子如下:
Clojure代码
(ns-interns 'clojure.contrib.math)

all-ns函数返回当前已加载命名空间的序列。当Clojure程序启动时,以下命名空间是默认加载的:
clojure.core , clojure.main , clojure.set , clojure.xml , clojure.zip和 user。在REPL中,除了上面的命名空间之外,
还会加载:clojure.contrib.repl-utils , clojure.contrib.seq-utils and clojure.contrib.str-utils。

namespace函数返回一个指定符号或者关键字对应的命名空间。

其他命名空间相关的函数在这儿就不进行讨论了,比如:ns-aliases , ns-imports , ns-map , ns-name , ns-publics , ns-refers , ns-unalias , ns-unmap和 remove-ns。

一些良好的输出(Some Fine Print)
Symbol对象拥有一个 String 名字和一个 String 命名空间( 调用ns),事实上它采用
字符串命名空间名代替了一个命名空间(Namespace )对象引用,
这就允许了它可能采用的是一个实际不存在的命名空间。

Var对象拥有对一个 Symbol对象( 调用sym),一个 Namespace对象( 调用ns)和一个做为其“根值”
("root value",调用root )的Object对象 的引用 。 Namespace对象拥有一个map的引用,这个map保存了
Symbol对象和 Var对象的联系(称作 mappings)。它们同样拥有一个包含 Symbol别名和 Namespace对象联系
(称作namespaces )的map。看看下面的类图,展示了在Clojure实现中,java类和接口的联系的一个
小子集。在Clojure中,术语"interning"通常指添加一个Symbol -to-Var映射到 Namespace。

ClassDiagram.png

Clojure-JVMλ语言(8) 元数据

博客分类: 翻译Clojure
clojurejavalispjvm
原帖地址:http://java.ociweb.com/mark/clojure/article.html#Metadata
作者:R. Mark Volkmann
译者:RoySong

元数据(Metadata)

Clojure的元数据是附属于符号或者集合的数据,而没有具体的逻辑值。两个对象在逻辑上相同,就像扑克中的两张
王相同,能够拥有不同的元数据。举个例子,元数据能够用来指明某张扑克是否是弯的。而对于大部分扑克游戏来说,
事实上扑克是否是弯的完全跟扑克的价值无关:
Clojure代码
(defstruct card-struct :rank :suit)

(def card1 (struct card-struct :king :club))
(def card2 (struct card-struct :king :club))

(println (== card1 card2)) ; same identity? -> false
(println (= card1 card2)) ; same value? -> true

(def card2 #^{:bent true} card2) ; adds metadata at read-time
(def card2 (with-meta card2 {:bent true})) ; adds metadata at run-time
(println (meta card1)) ; -> nil
(println (meta card2)) ; -> {:bent true}
(println (= card1 card2)) ; still same value despite metadata diff. -> true

某些元数据名字在Clojure中有特定的用途。:private 有一个布尔值来指明某个Var的 访问 权限是否被限制在它被定义的
命名空间中。 :doc是显示出某个Var的文档字符串。 :test拥有一个布尔值来指明某个无参函数是否测试函数。

:tag是一个字符串类名,或者描述某个Var的java类型的类对象,或者一个函数的返回类型。这些被称为“类型提示”(
"type hints"),使用类型提示能够提升性能。为了看到你的程序中什么地方Clojure采用了反射来识别类型,来作为性能
提升点,应该将全局变量*warn-on-reflection*设为true。

某些元数据会被Clojure编译器自动绑定到Var上。:file是用来定义Var的文件字符串名。 :line是文件中Var被定义地方
的整型行数。 :name是为Var提供名字的一个符号。 :ns是一个命名空间对象用来描述Var被定义的命名空间。 :macro是个
布尔值用来指明Var是个与函数相反的宏或者绑定。 :arglist是一个Vector的列表,其中每个vector都包含了一个函数接受
的所有参数的名字。回想一下,函数是可以拥有不止一组参数和函数体的。

函数和宏,都表现为一个Var对象的形式,拥有其关联的元数据。举个例子,在REPL中输入:(meta (var reverse))或者 ^#'reverse。其输出结果都跟下面的极其相似,不过是在一行上面:
Clojure代码
{
:ns #<Namespace clojure.core>,
:name reverse,
:file "core.clj",
:line 630,
:arglists ([coll]),
:doc "Returns a seq of the items in coll in reverse order. Not lazy."
}

Clojure Contrib 中repl-utils库里(clojure.contrib.repl-utils命名空间中 )的source函数,能够采用这些元数据来
检索指定函数或者宏的源代码,例子如下:
Clojure代码
(source reverse)

执行后产生以下输出:
Clojure代码
(defn reverse
"Returns a seq of the items in coll in reverse order. Not lazy."
[coll]
(reduce conj nil coll))

译者注:
元数据的概念,来源于百度百科:
元数据(Metadata)是描述其它数据的数据(data about other data),或者说是用于提供某种资源的有关信息的结构数据(structured data)。元数据是描述信息资源或数据等对象的数据,其使用目的在于:识别资源;评价资源;追踪资源在使用过程中的变化;实现简单高效地管理大量网络化 数据;实现信息资源的有效发现、查找、一体化组织和对使用资源的有效管理。

Clojure-JVMλ语言(9) 宏

博客分类: 翻译Clojurelisp
clojurejavalispjvm
原帖地址:http://java.ociweb.com/mark/clojure/article.html#Macros
作者:R. Mark Volkmann
译者:RoySong

宏(Macros)

宏被用来为语言添加新的功能结构。它们是在读取时(read-time)用来产生代码的代码。

函数总是要对它所有的参数求值,然而宏可以决定它的哪个参数被求值。这点对于实现诸如
(if condition
then-expr
else-expr
)这样的form非常重要。如果 condition
为true,那么只有
"then"表达式会被求值。如果condition
为false,那么只有"else"表达式会被求值。这代表着if
不能被实现为函数(实际上它是一个特殊form,而不是一个宏)。其他同样因为这个原因被实
现为宏的form包括and和or,因为它们需要做“短循环”("short-circuit")。

为了确定一个指定的操作是作为函数还是宏来实现,既可以在REPL中输入(doc name
),也可以
检查它的元数据。如果是个宏的话,它的元数据会包含一个:macro关键字并有一个true值。比如,
为了确定and的实现类型,在REPL中输入以下内容:
Clojure代码
((meta (var and)) :macro) ; long way -> true
(^#'and :macro) ; short way -> true

让我们通过编写和使用宏来轻松地实现一些例子。假设我们的代码中有很多地方需要进行不同的
操作基于某个某个数字是否真正接近于0,正数或者负数。我们想避免代码重复。这必须采用宏而不
是函数来实现,因为在某个条件下应该是一条语句被求值而不是三条(正,负,0)。采用defmacro
宏来创建一个宏:
Clojure代码
(defmacro around-zero [number negative-expr zero-expr positive-expr]
`(let [number# ~number] ; so number is only evaluated once
(cond
(< (Math/abs number#) 1e-15) ~zero-expr
(pos? number#) ~positive-expr
true ~negative-expr)))

读取器会将对around-zero宏的调用展开到对 let 特殊form的调用上去。let特殊form里面包含了一个
对cond函数的调用,cond的参数就是各项条件以及对应的返回值。在这儿采用let特殊form是为了在
第一个参数number接收的是一个表达式而非简单值得情况下提升效率。它只会对number求值一次,
然后在cond里面两次采用这个值。而系统自动生成变量声明(auto-gensym)number#是用来产生
一个独特的符号名而不会和其他符号名冲突。这样就允许了对 hygienic macros 的创建。

宏定义开头的后引号(“`”,又名语法引证syntax quote,编者注:键盘上1左边那个
键,不是单引号)避免了里面的所有内容被求值,直到引文结束。这代表着宏主体里面的内容都会按照
字面被展开,除了带波浪线的元素外(在上面的例子中,是 number , zero-expr , positive-expr
和 negative-expr)。而这些在语法引证 列表中前面带波浪线的符号,都会在展开时以其对应的值来代替。
在语法引证列表中的绑定如果它的值是序列,则可以在它的前面加上~@来代替它的个体值。

下面是使用这个宏的两个例子,预期的输出都是“+”:
Clojure代码
(around-zero 0.1 (println "-") (println "0") (println "+"))
(println (around-zero 0.1 "-" "0" "+")) ; same thing

如果需要在某处进行不止一次的求值,则采用do特殊form来包装它们。举个例子,如果number代表
温度,而我们用一个log函数来将它写入到日志文件中,那么我们会这么编写:
Clojure代码
(around-zero 0.1
(do (log "really cold!") (println "-"))
(println "0")
(println "+"))

为了验证这个宏是否正确地展开了,我们在REPL中输入以下内容:
Clojure代码
(macroexpand-1
'(around-zero 0.1 (println "-") (println "0") (println "+")))

输入结果如下,不过实际中没有缩进:
Clojure代码
(clojure.core/let [number3382auto__ 0.1]
(clojure.core/cond
(clojure.core/< (Math/abs number3382auto) 1.0E-15) (println "0")
(clojure.core/pos? number
3382auto) (println "+")
true (println "-")))

下面的函数采用了around-zero宏,并将返回值封装成单词:
Clojure代码
(defn number-category [number]
(around-zero number "negative" "zero" "positive"))

下面是一些使用函数的例子:
Clojure代码
(println (number-category -0.1)) ; -> negative
(println (number-category 0)) ; -> zero
(println (number-category 0.1)) ; -> positive

因为宏不会对其参数求值,所以未印证的函数名可以作为参数传递给宏,然后就可以构造对这些函数
的带参调用。函数定义无法做到这一点,与之替代的是传递一个匿名函数来包装对函数的调用。

下面有一个接收两个参数的宏,第一个参数是一个函数,它拥有一个参数用于接受一个弧度数值,就像
三角函数;第二个参数直接接收一个角度数值。如果在这儿采用函数定义来替代宏,我们就不得不采用
#(Math/sin %)这种形式来代替简单的 Math/sin。注意对 #号后缀的使用通过系统来生成独特的本地绑定
名,这通常是必要的来避免同其他绑定名冲突。#和~都只能在语法印证列表中使用。
Clojure代码
(defmacro trig-y-category [fn degrees]
`(let [radians# (Math/toRadians ~degrees)
result# (~fn radians#)]
(number-category result#)))

让我们实验一下,底下的调用预期的输出是 "zero", "positive", "zero"和"negative"。
Clojure代码
(doseq [angle (range 0 360 90)] ; 0, 90, 180 and 270
(println (trig-y-category Math/sin angle)))

宏的名字不能作为参数传递给函数。举个例子,一个宏的名称and不能传递给函数reduce。一种变通方案
是定义一个匿名函数来调用宏。举个例子,采用(fn [x y] (and x y)) 或者 #(and %1 %2)这样的形式。宏
会在读取时在匿名函数内部展开。当这个匿名函数作为参数传递给其他函数比如reduce,实际上是一个函数
对象而不是宏的名字被传递。

对宏的调用是在读取时被处理的。

Clojure-JVMλ语言(10) 并发

博客分类: 翻译Clojure
clojurejavalispjvm
原帖地址:http://java.ociweb.com/mark/clojure/article.html#Concurrency
作者:R. Mark Volkmann
译者:RoySong

并发(concurrency)

维基百科上面对并发有一个精确的定义:“并发是一种系统属性,支持多条指令实时交叉运行,并且有可能会
相互影响。而交叉的指令可能会在同一CPU的不同核心中的抢占式分时线程中执行,或者在不同的物理上分离的
CPU中执行。”并发编程的主要难度在于管理可变的共享状态。

采用锁机制来管理并发是艰难的。它需要决定哪些对象需要加锁以及什么时候需要加锁,当代码产生改动或者
是添加了新代码时,这些决定又需要重新估算。如果一个开发者在对象需要加锁时忘记加锁或者没能在正确的时间
加锁,会产生不可预料的后果,比如死锁(deadlocks )或者是竞态条件(race conditions )。如果对象不需要
加锁而对它进行了加锁,则会产生性能惩罚。

支持并发是很多开发者采用Clojure的主要原因。所有数据都是不变的,除非明确采用引用类型,比如:
Var , Ref , Atom 和Agent 来标识为可变。以上这些引用类型都采用了安全的方式来管理共享状态,将在下一节“
引用类型”("Reference Types ")中讨论。

我们能够很容易在一个新线程中运行Clojure函数,包括用户自定义的有名字的或者匿名的函数。之前在“Java
交互”("Java Interoperability ")章节中已经讨论过了。

因为Clojure能够使用所有的Java类和接口,所以它也能使用所有的Java并发功能。在
"Java Concurrency In Practice "书中包含了很多这方面的例子。这本书中有很多采用Java管理并发的良好建议,
但是要遵循这些建议并不是那么简单。在大多数情况下,采用Clojure的引用类型比采用Java并发要轻松得多。

除了引用类型之外,Clojure还提供了很多函数来帮助Clojure代码在多线程中运行。

future宏能够让它内部的表达式在线程池( CachedThreadPool )中不同的线程中运行, Agents 也同样采用了
线程池。这对于某些需要长时间运行而不需要即时得到返回结果的表达式是非常有用的。表达式的结果会被保存在
一个非关联对象中由future返回。如果 future宏主体还没执行完毕,而已经需要结果,当前的线程就阻塞直到返回
结果为止。当代理线程池中的某条线程使用完毕后,应该在某个点上调用shutdown-agents来使线程停止和应用程序
退出。

为了论证future的使用,在 "Defining Functions "章节末尾,一个println函数添加到derivative函数中。它帮助
确认了future啥时候被执行。注意下面代码输出结果的顺序:
Clojure代码
(println "creating future")
(def my-future (future (f-prime 2))) ; f-prime is called in another thread
(println "created future")
(println "result is" @my-future)
(shutdown-agents)

如果f-prime函数并没有很快地结束,那么代码的输出将会是:
Clojure代码
creating future
created future
derivative entered
result is 9.0

pmap函数实现了并发地将某个函数应用于某个集合的每个元素。当函数应用的时间消耗超过了线程管理的日常开支时,
采用pmap能够比 map获得更好的性能表现。

clojure.parallel命名空间提供了更多的函数来帮助实现并发代码。它们包含
par , pdistinct , pfilter-dupes , pfilter-nils , pmax , pmin , preduce , psort , psummary和 pvec。