作者: xumingming | 可以转载, 但必须以超链接形式标明文章原始出处和作者信息及版权声明
http://xumingming.sinaapp.com/302/clojure-functional-programming-for-the-jvm-clojure-tutorial/
本文翻译自:Clojure – Functional Programming for the JVM 转载请注明出处
内容列表
简介 | 条件处理 | 引用类型 |
函数式编程 | 迭代 | 编译 |
Clojure概述 | 递归 | 自动化测试 |
开始吧 | 谓词 | 编辑器和IDE |
Clojure语法 | 序列 | 桌面程序 |
REPL | 输入输出 | Web应用 |
Bindings | 解构 | 数据库 |
集合 | 名字空间 | 类库 |
StructMaps | 元数据 | 结论 |
定义函数 | 宏 | 引用 |
和Java的互操作 | 并发 |
简介
这篇文章的目的是以通俗易懂的方式引导大家进入Clojure的世界。文章涵盖了cojure的大量的特性, 对每一个特性的介绍我力求简介。你不用一条一条往下看,尽管跳到你感兴趣的条目。
请把你的意见,建议发送到mark@ociweb.com(如果是对文章翻译的建议,请直接在文章下面留言:
http://xumingming.sinaapp.com/302/clojure-tutorial/)。我对下面这样的建议特别感兴趣:
- 你说是X, 其实是Y
- 你说是X, 但其实说Y会更贴切
- 你没有提到X, 但是我认为X是一个非常重要的话题
对这篇文章的更新可以在http://www.ociweb.com/mark/clojure/找到, 同时你也可以在http://www.ociweb.com/mark/stm/找到有关Software Transactional Memory的介绍, 以及Clojure对STM的实现。
这篇文章里面的代码示例里面通常会以注释的形式说明每行代码的结果/输出,看下面的例子:
1
2
|
(+ 1 2) ; showing return value: 3 ( println "Hello" ) ; return nil, showing output:Hello |
函数式编程
函数式编程是一种强调函数必须被当成第一等公民对待, 并且这些函数是“纯”的编程方式。这是受lambda表达式启发的。纯函数的意思是同一个函数对于同样同样的参数,它的返回值始终是一样的 — 而不会因为前一次调用修改了某个全局变量而使得后面的调用和前面调用的结果不一样。这使得这种程序十分容易理解、调试、测试。它们没有副作用
— 修改某些全局变量, 进行一些IO操作(文件IO和数据库)。状态被维护在方法的参数上面, 而这些参数被存放在栈(stack)上面(通常通过递归调用), 而不是被维护在全局的堆(heap)上面。这使得方面可以被执行多次而不用担心它会更改什么全局的状态(这是非常重要的特征,等我们讨论事务的时候你就会意识到了)。这也使得高级编译器为了提高代码性能而对代码进行重排(reording)和并行化(parallelizing)成为可能。(并行化代码现在还很少见)
在实际生活中,我们的程序是需要一定的副作用的。Haskel的主力开发Simon Peyton-Jones曾经曰过:
“到最后,任何程序都需要修改状态,一个没有副作用的程序对我们来说只是一个黑盒, 你唯一可以感觉到的是:这个黑盒在变热。。”(http://oscon.blip.tv/file/324976)
问题的关键是我们要控制副作用的范围, 清晰地定位它们,避免这种副作用在代码里面到处都是。
把函数当作“第一公民”的语言可以把函数赋值给一个变量,作为参数来调用别的函数, 同时一个函数也可以返回一个函数。可以把函数作为返回值的能力使得我们选择之后程序的行为。接受函数作为参数的函数我们称为“高阶函数”。从某个方面来说,高阶函数的行为是由传进来的函数来配置的,这个函数可以被执行任意次,也可以从不执行。
函数式语言里面的数据是不可修改的。这使得多个线程可以在不用锁的情况下并发地访问这个数据。因为数据不会改变,所以根本不需要上锁。随着多核处理器的越发流行,函数式语言对并发语言的简化可能是它最大的优点。如果所有这些听起来对你来说很有吸引力而且你准备来学学函数式语言,那么你要有点心理准备。许多人觉得函数式语言并不比面向对象的语言难,它们只是风格不同罢了。而花些时间学了函数式语言之后可以得到上面说到的那些好处,我想还是值得的。比较流行的函数式语言有:Clojure,Common
Lisp, Erlang,
F#, Haskell,
ML, OCaml,
Scheme, Scala. Clojure和Scala是Java Virtual Machine (JVM)上的语言. 还有一些其它基于JVM的语言:Armed Bear Common Lisp (ABCL),OCaml-Java
and Kawa (Scheme).
Clojure是一个动态类型的,运行在JVM(JDK5.0以上),并且可以和java代码互操作的函数式语言。这个语言的主要目标之一是使得编写一个有多个线程并发访问数据的程序变得简单。
Clojure的发音和单词closure是一样的。Clojure之父是这样解释Clojure名字来历的
“我想把这就几个元素包含在里面: C (C#), L (Lisp) and J (Java). 所以我想到了 Clojure, 而且从这个名字还能想到closure;它的域名又没有被占用;而且对于搜索引擎来说也是个很不错的关键词,所以就有了它了.”
很快Clojure就会移植到.NET平台上了. ClojureCLR是一个运行在Microsoft的CLR的Clojure实现. 在我写这个入门教程的时候ClojureCLR已经处于alpha阶段了.
在2011年7月, ClojureScript项目开始了,这个项目把Clojure代码编译成Javascript代码:看这里https://github.com/clojure/clojurescript.
Clojure是一个开源语言, licence:Eclipse Public License v 1.0 (EPL). This is a very liberal license. 关于EPL的更多信息看这里:http://www.eclipse.org/legal/eplfaq.php
.
运行在JVM上面使得Clojure代码具有可移植性,稳定性,可靠的性能以及安全性。同时也使得我们的Clojure代码可以访问丰富的已经存在的java类库:文件 I/O, 多线程, 数据库操作, GUI编程, web应用等等等等.
Clojure里面的每个操作被实现成以下三种形式的一种: 函数(function), 宏(macro)或者special form. 几乎所有的函数和宏都是用Clojure代码实现的,它们的主要区别我们会在后面解释。Special forms不是用clojure代码实现的,而且被clojure的编译器识别出来. special forms的个数是很少的, 而且现在也不能再实现新的special forms了. 它们包括:catch,def,do,dot
(‘.’),finally,fn,if,let,loop,monitor-enter,monitor-exit,new,quote,recur,set!,throw,try
和var.
Clojure提供了很多函数来操作序列(sequence), 而序列是集合的逻辑视图。很多东西可以被看作序列:Java集合, Clojure的集合, 字符串, 流, 文件系统结构以及XML树. 从已经存在的clojure集合来创建新的集合的效率是非常高的,因为这里使用了persistent data structures的技术(这对于clojure在数据不可更改的情况下,同时要保持代码的高效率是非常重要的)。
Clojure提供三种方法来安全地共享可修改的数据。所有三种方法的实现方式都是持有一个可以开遍的引用指向一个不可改变的数据。Refs 通过使用Software
Transactional Memory(STM)来提供对于多块共享数据的同步访问。Atoms 提供对于单个共享数据的同步访问。Agents
提供对于单个共享数据的异步访问。这个我们会在 “引用类型”一节详细讨论。
Clojure是Lisp的一个方言. 但是Clojure对于传统的Lisp有所发展。比如, 传统Lisp使用car
来获取链表里面的第一个数据。而Clojure使用first。有关更多Clojure和Lisp的不同看这里:
http://clojure.org/lisps.
Lisp的语法很多人很喜欢,很多人很讨厌, 主要因为它大量的使用圆括号以及前置表达式. 如果你不喜欢这些,那么你要考虑一下是不是要学习Clojure了 。许多文件编辑器以及IDE会高亮显示匹配的圆括号, 所以你不用担心需要去人肉数有没有多加一个左括号,少写一个右括号. 同时Clojure的代码还要比java代码简洁. 一个典型的java方法调用是这样的:
1
|
methodName(arg1, arg2, arg3); |
而Clojure的方法调用是这样的:
1
|
(function-name arg1 arg2 arg3) |
左括号被移到了最前面;逗号和分号不需要了. 我们称这种语法叫: “form”. 这种风格是简单而又美丽:Lisp里面所有东西都是这种风格的.要注意的是clojure里面的命名规范是小写单词,如果是多个单词,那么通过中横线连接。
定义函数也比java里面简洁。Clojure里面的println
会在它的每个参数之间加一个空格。如果这个不是你想要的,那么你可以把参数传给str
,然后再传给println
.
1
2
3
4
|
// Java public hello(String name) { System.out.println( "Hello, " + name); } |
1
2
3
|
; Clojure ( defn hello [ name ] ( println "Hello," name)) |
Clojure里面大量之用了延迟计算. 这使得只有在我们需要函数结果的时候才去调用它. “懒惰序列” 是一种集合,我们之后在需要的时候才会计算这个集合理解面的元素. 只使得创建无限集合非常高效.
对Clojure代码的处理分为三个阶段:读入期,编译期以及运行期。在读入期,读入期会读取clojure源代码并且把代码转变成数据结构,基本上来说就是一个包含列表的列表的列表。。。。在编译期,这些数据结构被转化成java的bytecode。在运行期这些java bytecode被执行。函数只有在运行期才会执行。而宏在编译期就被展开成实际对应的代码了。
Clojure代码很难理解么?想想每次你看到java代码里面那些复杂语法比如: if
,for
, 以及匿名内部类, 你需要停一下来想想它们到底是什么意思(不是那么的直观),同时如果想要做一个高效的Java工程师,我们有一些工具可以利用来使得我们的代码更容易理解。同样的道理,Clojure也有类似的工具使得我们可以更高效的读懂clojure代码。比如:let
,apply
,map
,filter
,reduce
以及匿名函数 … 所有这些我们会在后面介绍.
让我们开始吧
Clojure是一个相对来说很新的语言。在经过一些年的努力之后,Clojure的第一版是在2007年10月16日发布的。Clojure的主要部分被称为 “Clojure proper” 或者 “core”。你可以从这里下载:http://clojure.org/downloads. 你也可以使用Leiningen。最新的源代码可以从它的Git库下载.
“Clojure Contrib“是一个大家共享的类库列表。其中有些类库是成熟的,被广泛使用的并且最终可能会被加入Clojure Proper的。但是也有些库不是很成熟,没有被广泛使用,所以也就不会被包含在Conjure Proper里面。所以Clojure Proper里面是鱼龙混杂,使用的时候要自己斟酌,文档在这里:http://richhickey.github.com/clojure-contrib/index.html
对于一个Clojure Contrib, 有三种方法可以得到对应的jar包. 首先你可以下载一个打包好的jar包。其次你可以用maven 来自己打个jar包. Maven可以从这里下载http://maven.apache.org/. 打包命令是 “mvn package
“. 再其次你可以用ant. ant可以从这里下载http://ant.apache.org/。命令是:
“ant -Dclojure.jar={path}
“.
要从最小的源代码来编译clojure, 我们假设你已经安装了Git 和Ant , 运行下面的命令来下载并且编译打包Clojure Proper和Clojure Contrib:
1
2
3
4
5
6
7
|
git clone git: //github .com /richhickey/clojure .git cd
ant clean jar cd
git clone git: //github .com /richhickey/clojure-contrib .git cd
ant -Dclojure.jar=.. /clojure/clojure .jar clean jar |
下一步,写一个脚本来运行Read/Eval/Print Loop (REPL) 以及运行 Clojure 程序. 这个脚本通常被命名为”clj”. 怎么使用REPL我们等会再介绍. Windows下面,最简单的clj脚本是这样的(UNIX, Linux以及 Mac OS X下面把 %1 改成 $1):
1
|
java -jar /path/clojure .jar %1 |
这个脚本假定java
在你的PATH
环境变量里面. 为了让这个脚本更加有用:
- 把经常使用的JAR包比如 “Clojure Contrib” 以及数据库driver添加到classpath里面去(
-cp
). - 使clj更好用:用rlwrap(利用keystrokes来支持的) 或者JLine来得到命令提示以及命令历史提示。
- 添加一个启动脚本来设置一些特殊变量(比如
*print-length*和
*print-level*
), 加载一些常用的、不再java.lang 里面的包
加载一些常用的不再clojure.core
里面的函数并且定义一些常用自定义的函数.
使用这个脚本来启动REPL我们会等会介绍. 用下面这个命令来运行一个clojure脚本(通常以clj为后缀名):
1
|
clj source - file -path |
更多细节看这里http://clojure.org/getting_started 以及这里:http://clojure.org/repl_and_main。同时Stephen Gilardi 还提供了一个脚本:http://github.com/richhickey/clojure-contrib/raw/master/launchers/bash/clj-env-dir。
为了更充分的利用机器的多核,你应该这样来调用: “java -server ...
“.
提供给Clojure的命令行参数被封装在预定义的变量*command-line-args*里面。
Clojure语法
Lisp方言有一个非常简洁的语法 — 有些人觉得很美的语法。数据和代码的表达形式是一样的,一个列表的列表很自然地在内存里面表达成一个tree。(a b c)表示一个对函数a的调用,而参数是b和c。如果要表示数据,你需要使用'(a b c)
o或者(quote (a b c))
。通常情况下就是这样了,除了一些特殊情况 — 到底有多少特殊情况取决于你所使用的方言。
我们把这些特殊情况称为语法糖。语法糖越多代码写起来越简介,但是同时我们也要学习更多的东西以读懂这些代码。这需要找到一个平衡点。很多语法糖都有对应的函数可以调用。到底语法糖是多了还是少了还是你们自己来判断吧。
下面这个表格简要地列举了Clojure里面的一些语法糖, 这些语法糖我们会在后面详细讲解的,所以如果你现在没办法完全理解它们不用担心。
作用 | 语法糖 | 对应函数 |
---|---|---|
注释 | ; text
单行注释 |
宏(comment text)可以用来写多行注释 |
字符 (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 |
N/A |
正则表达式 | #"pattern" |
(re-pattern pattern) |
逗号被当成空白(通常用在集合里面用来提高代码可读性) | , (逗号) |
N/A |
链表(linked list) | '(items) (不会evaluate每个元素) |
(list items) 会evaluate每个元素 |
vector(和数组类似) | [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) |
给symbol或者集合绑定元数据 | #^{key-value-pairs} object 在读入期处理 |
(with-meta object metadata-map) 在运行期处理 |
获取symbol或者集合的元数据 | ^object |
(meta object) |
获取一个函数的参数列表(个数不定的) | & name |
N/A |
函数的不需要的参数的默认名字 | _ (下划线) |
N/A |
创建一个java对象(注意class-name后面的点) | (class-name. args) |
(new class-name args) |
调用java方法 | (. class-or-instance method-name args) 或者(.method-name class-or-instance args) |
N/A |
串起来调用多个函数,前面一个函数的返回值会作为后面一个函数的第一个参数;你还可以在括号里面指定额外参数;注意前面的两个点 | (.. class-or-object (method1 args) (method2 args) ...) |
N/A |
创建一个匿名函数 | #(single-expression) 用 % (等同于 %1 ), %1 , %2来表示参数 |
(fn [arg-names] expressions) |
获取Ref, Atom 和Agent对应的valuea | @ref |
(deref ref) |
get Var object instead ofthe value of a symbol (var-quote) |
#'name |
(var name) |
syntax quote (使用在宏里面) | ` |
none |
unquote (使用在宏里面) | ~value |
(unquote value) |
unquote splicing (使用在宏里面) | ~@value |
none |
auto-gensym (在宏里面用来产生唯一的symbol名字) | prefix# |
(gensym prefix ) |
对于二元操作符比如 +和*, Lisp方言使用前置表达式而不是中止表达式,这和一般的语言是不一样的。比如在java里面你可能会写a + b + c
, 而在Lisp里面它相当于(+ a b c) 。这种表达方式的一个好处是如果操作数有多个,那么操作符只用写一次
. 其它语言里面的二元操作符在lisp里面是函数,所以可以有多个操作数。
Lisp代码比其它语言的代码有更多的小括号的一个原因是Lisp里面不使用其它语言使用的大括号,比如在java里面,方法代码是被包含在大括号里面的,而在lisp代码里面是包含在小括号里面的。
比较下面两段简单的Java和Clojure代码,它们实现相同的功能。它们的输出都是: “edray” 和 “orangeay”.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
// This is Java code. public class PigLatin { public static String pigLatin(String word) { char firstLetter = word.charAt(0); if ( "aeiou" .indexOf(firstLetter) != -1) "ay" ; return word.substring(1) + firstLetter + "ay" ; } public static void main(String args [ ] ) { System.out. println (pigLatin( "red" )); System.out. println (pigLatin( "orange" )); } } |
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
; 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支持所有的常见数据类型比如 booleans (true
and false
), 数字, 高精度浮点数, 字符(上面表格里面提到过 ) 以及字符串. 同时还支持分数 — 不是浮点数,因此在计算的过程中不会损失精度.
Symbols是用来给东西命名的. 这些名字是被限制在名字空间里面的,要么是指定的名字空间,要么是当前的名字空间. Symbols的值是它所代表的名字的值. 要使用Symbol的值,你必须把它用引号引起来.
关键字以冒号打头,被用来当作唯一标示符,通常用在map里面 (比如:red
, :green
和
).
:blue
和任何语言一样,你可以写出很难懂的Clojure代码。遵循一些最佳实践可以避免这个。写一些简短的,专注自己功能的函数可以使函数变得容易读,测试以及重复利用。经常使用“抽取方法”的模式来对你的代码进行重构。高度内嵌的函数是非常难懂得,千万不要这么写, 你可以使用let来帮助你。把匿名函数传递给命名函数是非常常见的,但是不要把一个匿名函数传递给另外一个匿名函数, 这样代码就很难懂了。
REPL
REPL 是read-eval-print loop的缩写. 这是Lisp的方言提供给用户的一个标准交互方式,如果用过python的人应该用过这个,你输入一个表达式,它立马再给你输出结果,你再输入。。。如此循环。这是一个非常有用的学习语言,测试一些特性的工具。
为了启动REPL, 运行我们上面写好的clj脚本。成功的话会显示一个”user=>
“. “=>
” 前面的字符串表示当前的默认名字空间。“=>”后面的则是你输入的form以及它的输出结果。 下面是个简单的例子:
1
2
3
4
|
user=> (def n 2) #'user/n user=> (* n 3) 6 |
def
是一个 special form, 它相当于java里面的定义加赋值语句. 它的输出表示一个名字叫 “n
” 的symbol被定义在当前的名字空间 “user
” 里面。
要查看一个函数,宏或者名字空间的文档输入(doc name)。看下面的例子:
1
2
3
4
5
6
7
|
( 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会自动加载这个宏的。
如果要加载并且执行文件里面的clojure代码那么使用这个命令(load-file "file-path")
. Clojure源文件一般以.clj作为后缀。
如果要退出REPL,在Windows下面输出ctrl-z然后回车, 或者直接 ctrl-c; 在其它平台下 (包括UNIX, Linux 和 Mac OS X), 输入 ctrl-d.
Bindings
Clojure里面是不支持变量的。它跟变量有点像,但是在被赋值之前是不允许改的,包括:全局binding, 线程本地(thread local)binding, 以及函数内的本地binding, 以及一个表达式内部的binding。
def
这个special form 定义一个全局的 binding,并且你还可以给它一个”root value” ,这个root value在所有的线程里面都是可见的,除非你给它赋了一个线程本地的值.def
也可以用来改变一个已经存在的binding的root value —— 但是这是不被鼓励的,因为这会牺牲不可变数据所带来的好处。
函数的参数是只在这个函数内可见的本地binding。
let
这个special form 创建局限于一个 当前form的bindings. 它的第一个参数是一个vector, 里面包含名字-表达式的对子。表达式的值会被解析然后赋给左边的名字。这些binding可以在这个vector后面的表达式里面使用。这些binding还可以被多次赋值以改变它们的值,let命令剩下的参数是一些利用这个binding来进行计算的一些表达式。注意:如果这些表达式里面有调用别的函数,那么这个函数是无法利用let创建的这个binding的。
宏 binding
跟let 类似
, 但是它创建的本地binding会暂时地覆盖已经存在的全局binding. 这个binding可以在创建这个binding的form以及这个form里面调用的函数里面都能看到。但是一旦跳出了这个binding
那么被覆盖的全局binding的值会回复到之前的状态。
从 Clojure 1.3开始, binding只能用在 动态变量(dynamic var)上面了. 下面的例子演示了怎么定一个dynamic var。另一个区别是let
是串行的赋值的, 所以后面的binding可以用前面binding的值, 而binding
是不行的.
要被用来定义成新的、本地线程的、用binding来定义的binding有它们自己的命名方式:她们以星号开始,以星号结束。在这篇文章里面你会看到:*command-line-args*
,*agent*
,*err*
,*flush-on-newline*
,*in*
,*load-tests*
,*ns*
,*out*
,*print-length*
,*print-level*
and*stack-trace-depth*
.要使用这些binding的函数会被这些binding的值影响的。比如给*out*一个新的binding会改变println函数的输出终端。
下面的例子介绍了def
,let
和binding
的用法。
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
( 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 ] ; ( 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 ] ; 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) |
上面代码的输出是这样的:
1
2
3
4
5
6
7
8
9
|
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提供这些集合类型: list, vector, set, map。同时Clojure还可以使用Java里面提供的将所有的集合类型,但是通常不会这样做的, 因为Clojure自带的集合类型更适合函数式编程。
Clojure集合有着java集合所不具备的一些特性。所有的clojure集合是不可修改的、异源的以及持久的。不可修改的意味着一旦一个集合产生之后,你不能从集合里面删除一个元素,也往集合里面添加一个元素。异源的意味着一个集合里面可以装进任何东西(而不必须要这些东西的类型一样)。持久的以为着当一个集合新的版本产生之后,旧的版本还是在的。CLojure以一种非常高效的,共享内存的方式来实现这个的。比如有一个map里面有一千个name-valuea pair, 现在要往map里面加一个,那么对于那些没有变化的元素,
新的map会共享旧的map的内存,而只需要添加一个新的元素所占用的内存。
有很多核心的函数可以用来操作所有这些类型的集合。。多得以至于无法在这里全部描述。其中的一小部分我们会在下面介绍vector的时候介绍一下。要记住的是,因为clojure里面的集合是不可修改的,所以也就没有对集合进行修改的函数。相反clojure里面提供了一些函数来从一个已有的集合来高效地创建新的集合 — 使用persistent data structures。同时也有一些函数操作一个已有的集合(比如vector)来产生另外一种类型的集合(比如LazySeq),
这些函数有不同的特性。
提醒: 这一节里面介绍的Clojure集合对于学习clojure来说是非常的重要。但是这里介绍一个函数接着一个函数,所以你如果觉得有点烦,有点乏味,你可以跳过,等用到的时候再回过头来查询。
count
返回集合里面的元素个数,比如:
1
|
( count [ 19 "yellow" true ] ) ; -> 3 |
conj
函数是 conjoin的缩写, 添加一个元素到集合里面去,到底添加到什么位置那就取决于具体的集合了,我们会在下面介绍具体集合的时候再讲。
reverse
把集合里面的元素反转。
1
|
(reverse [ 2 4 7 ] ) ; -> (7 4 2) |
map
对一个给定的集合里面的每一个元素调用一个指定的方法,然后这些方法的所有返回值构成一个新的集合(LazySeq)返回。这个指定了函数也可以有多个参数,那么你就需要给map多个集合了。如果这些给的集合的个数不一样,那么执行这个函数的次数取决于个数最少的集合的长度。比如:
1
2
3
|
; The next line uses an anonymous function that adds 3 to its argument.
|