clojure 新手指南-目录 - climbdream的个人空间 - 开源中国社区
https://my.oschina.net/clopopo/blog/141776
无意中在一个博客上发现《clojure 新手指南系列》系列。感觉很不错,于是决定翻译下来与大家共享。
先列出整个系列的目录:
设置与安装
使用REPL来求值
对复杂表达式求值
代码保护
判断&基本类型
全局绑定&匿名函数
定义函数
参数&重载
元数据
与java交互
正则表达式
本地绑定&词法作用域
序列&向量
Hash-Maps ,Array-Maps & Sorted Maps
可变性
基本迭代&递归
Map , Reduce & Filter
总共17篇,争取每天翻译一篇,争取不太监
clojure 新手指南(1)设置&安装 - climbdream的个人空间 - 开源中国社区
https://my.oschina.net/clopopo/blog/141813
这篇设置指南的目标读者是那些没有或者很少编程经验的人。内容包括如何访问windows 命令行接口,如何确定java是否被正确安装,如何在REPL上运行clojure。
步骤一:使用命令行接口
所有的操作系统都包含一个命令行接口。这种纯文字接口建立了人机交流的通讯入口,让你借此来管理系统应用和服务。我们将会使用这个接口来设置、安装、和运行clojure程序。
在windos系统中,命令行接口就是为人所知的“命令行提示符”。(如何进入就不翻译了)
步骤二:检查java
clojure需要java才能运行。换句话说,clojure会被编译成java字节码,这些字节码最终会被依次编译成机器码并被操作系统执行。
windows系统默认情况下并不会安装java。你可以用命令行接口检查检查。在命令行中,敲下“java -version" 来确认一下是否安装。
如果正确安装,命令行会做出下面成功的回应:
java -version java version "1.6.0_20" Java(TM) SE Runtime Environment (build 1.6.0_20-b02-279-10M3065) Java HotSpot(TM) 64-Bit Server VM (build 16.3-b01-279, mixed mode)
在安装clojure之前,务必确保已经正确安装java。
步骤三:安装clojure
首先下载当前最新的稳定版本clojure,将其解压到指定的目录下。
目录结构如下:
我们唯一需要的只是clojure-[版本].jar文件。
步骤四:通过REPL运行clojure
关于REPL:REPL 是 read-eval-print loop的缩写。它能让你一行行的敲入代码并能看到运行结果。
终于可以载入clojure程序了。既然clojure离不开java,所以你需要通过使用java命令来指定clojure文件来运行它。
使用java运行clojure(注意路径和文件名的正确性):
java -cp clojure.jar clojure.main Clojure 1.4.0 user=>
so easy !现在你的命令行接口已经成功运行了一个clojure REPL。提示符现在变 成了”user=>",你既可以敲入clojure表达式,也可以加载clojure程序了。
可选方案一:clojure 与clojure contrib
在进入REPL的时候有一个可选方案:同时加载clojure的增强扩展包,你可以点击这里下载。解压缩后,将contrib.jar拷贝到clojure主目录(包含clojure.jar的目录 ),然后在敲击下面命令:
java -cp contrib.jar;clojure.jar clojure.main Clojure 1.4.0 user=>
可选方案二:clojure 与 JLine
当clojure的REPL接管整个命令行接口时,有一些非常有用的特性变得不可用了。特别是使用上下键去查看历史命令,或者是使用左右键去逐字扫描(step through ,这个翻译不知道对不对)当前行。
如果你认为这些特性很有用,你会很高兴的发现这些特性也不难设置。只是简单的下载这个文件,放到你的clojure主目录。然后使用下面命令:
java -cp jline-0.9.94.jar;clojure.jar jline.ConsoleRunner clojure.main Clojure 1.4.0 user=>
我在clojure1.4.0 的REPL下试了试上下左右键,貌似不用jline就可以呀。难道改进了?(求指点)可选方案三:clojure 与 contrib 、jline
这个不用解释,直接上代码:
java -cp jline-0.9.94.jar;contrib.jar;clojure.jar jline.ConsoleRunner clojure.main Clojure 1.4.0 user=>
clojure 新手指南(2)使用REPL求值 - climbdream的个人空间 - 开源中国社区
https://my.oschina.net/clopopo/blog/142086
Clojure 拥有动态语言的所有好处。这意味着你可以在程序被加载后依然可以改变它,并且不用采取额外的步骤去编译代码。你既不用停止也不用重启正在运行的应用就可以让修改生效。这对于其他语言来说可是一个非常显著的优势,特别是如果你正打算将变化动态地(不用停止和重启服务器)呈献给用户时。
要想改变一个已经加载的程序,你唯一需要的是使用REPL去加载变化的地方。REPL是一个独立的程序,Clojure利用它提供给你和它的编译器直接交互的功能,当然也能与已经加载的程序进行交互。REPL代表读取(Read)、求值(Evaluate)、打印(Print)和 循环(Loop)。为了使用REPL,你只需要使用操作系统提供的命令行来运行它。
表达式求值
打开你的REPL,随机敲入一些字符。很大几率上Clojure会及时地作出相应一个错误。它可不是什么值都会接受。
=>ughjava.lang.Exception: Unable to resolve symbol: ugh in this context..
实际上你会发现Clojure只能对符合语法规则的表达式求值。
有一点需要记住,所有的表达式都会返回值。即使这个表达式什么也不做,它也会返回值,哪怕仅仅是一个'nil'(类似java中的null)。
=>(do) //先不用管do是做什么的,其实什么也不做nil
这是一个很好的特性,因为如果clojure要是什么都不返回的话,你不知道它是否已经执行完毕还是陷入了死循环。
求字面值
我们之前求值都是针对“表达式”(expression)。我们为什么不用“代码”(code)或者“声明”(statements)来代替“表达式”这个词呢?一个理由是“表达式”这个词本身就包含了“代码"或者”声明”这两个概念。拎一个重要的原因是clojure可不止是只能对代码求值。clojure可以对“数据”(data)求值,这一点与其他语言不同。对clojure来说,代码即数据。(感觉这个例子体现的不是太明显)
数据被求值时仅仅返回自身
=>21.4221.42=>"a string of characters""a string of characters"
Clojure的数据操作可不仅仅是字符串或者数字,实际上它支持一套非常丰富的数据类型和数据结构。但是在我们深入clojure之前,还是得先对着门语言多一些了解。
clojure 新手指南(3)复杂表达式求值 - climbdream的个人空间 - 开源中国社区
https://my.oschina.net/clopopo/blog/142111
为了理解复杂的表达式和对它的操作,一个首要的前提就是理解”前缀表达式“。这可能会花费你一点时间来习惯它。不过我相信你会很快的爱上这种规则的。你想想,如果你要对多个值进行同一种运算,你只用写一个运算符在第一个值的最前面,而不是写多个运算符在中间。不信就看下面的例子:
普通: 1 + 2 + 3 + 4 + 5 + 6 + 8 + 9前缀: + 1 2 3 4 5 6 7 8 9
抛开前缀表示法不说,一个复杂表达式可以看出是一个单独的操作,或者是一组操作。它们既可以接收参数,也能向外输出,在整个表达式计算完成的时候返回计算结果。一句话,复杂的表达式可以做任何你想让它做的事情。让我们看一个非常简单的例子:(+ 1 2)
在对这个式子求值前,我们先去看一看clojure在求值期间会做哪些工作:
首先,clojure会遇到左括号,表明这是一个列表(表达式的形式)的开始。这个列表包含了它要处理的所有东西。clojure永远会把左括号的第一个元素当做是一个操作符(这也是采用前缀表达式的原因),这种方式使得lisp方言语法异常的简单。在我们的例子中,操作符指的就是一个函数。(函数其实可以看出是针对其参数需要做哪些操作的说明)。当clojure遇到一个右括号,表明这个列表的结束。这种解析是递归的,因为列表中的元素依然可能是列表。这种表达方式还有另一个名字:S表达式。(lisp 的含义就是 list processor 即列表处理)。
S表达式看起来可能很直观,但对它的理解是非常非常重要的。这是学习一切lisp方言的基础。Clojure在执行函数(例如+)之前,首先会对其所有的参数进行顺序(从左到右)求值并返回自己的结果。然后函数会针对这些参数的返回结果进行相应的求值运算。返回最终的结果。
上面例子中,”+"是函数,参数都是数字字面值。我们说过字面值求值后返回自己。所以整个操作就是将“+”运用到1和2之上。得到的最终结果就是3。再考虑一下下面这个例子
(+ 2 (- 8 3))
上面例子稍微复杂了一点,因为第二个参数不是单纯的字面值。这个求值也非常简单,clojure在对第二个参数(- 8 3)进行求值时采取的依然是之前的策略,得到结果5。最终 "+"会作用在2和5之上,最终结果返回7。所以我们说这种解析方式是递归的。S表达式规则虽然简单,但是真的是变化无穷啊。
再来看一个例子(检测对前缀表达式的理解):
=>(- 8 3 2 1 -6 34 12 4 2 6 4 -23 12 4) -47
了解完上面,接下来做点啥呢?运行点例子也许是个好主意,但是还有更好的方法。让我们看一看内置函数的源码来了解一下。查看某个函数的源码很简单,还是调用函数来做。我们会使用一个名为“source”的函数:(source 【函数名】)
让我们查看函数'-'(减号)的源码
=>(source -) ;;依然是S表达式 (defn - "If no ys are supplied, returns the negation of x, else subtracts the ys from x and returns the result." {:inline (fn [& args] `(. clojure.lang.Numbers (minus ~ @args))) :inline-arities #{1 2} :added "1.0"} ([x] (. clojure.lang.Numbers (minus x))) ([x y] (. clojure.lang.Numbers (minus x y))) ([x y & more] (reduce - (- x y) more))) nil
这个比预想的要多的多了,一个减号居然有这么多代码量。现在不用担心不理解上面的代码。因为这里面包含了很多未知的知识。我们只用知道一点是,我们的”-“可以操作任意个参数的能力,而这一切归功一个叫做”reduce“的函数。我们不用去调用”reduce“的源码,只用看看相关描述即可。我们可以使用”doc“函数:
=>(doc reduce) ------------------------- clojure.core/reduce ([f coll] [f val coll]) f should be a function of 2 arguments. If val is not supplied, returns the result of applying f to the first 2 items in coll, then applying f to that result and the 3rd item, etc. If coll contains no items, f must accept no arguments as well, and reduce returns the result of calling f with no arguments. If coll has only 1 item, it is returned and f is not called. If val is supplied, returns the result of applying f to val and the first item in coll, then applying f to that result and the 2nd item, etc. If coll contains no items, returns val and f is not called. nil
这里的reduce和python中的reduce函数含义是一样。
clojure 新手指南(4)代码保护 - climbdream的个人空间 - 开源中国社区
https://my.oschina.net/clopopo/blog/142297
有时候,你可能需要防止一个表达式或者部分表达式被执行。这种就需要一种称为“代码保护”的技术。这项技术使用起来非常简单,就是在表达式前面加上一个单引号“ ‘ ”。clojure 遇到这种前缀加上单引号的表达式就会直接跳过求值,直接把其当做一种叫做“符号”的数据结构。
=>(+ 4 5 3)12=>'(+ 4 5 3)(+ 4 5 3)=>(str '(+ 4 5 3) " is protected while " (+ 4 5 3) " is evaluated.")"(+ 4 5 3) is protected while 12 is evaluated."
关于符号:这里的单引号实际上是另一种形式,叫做quote。'(1 2 3)和(quoto (1 2 3))只是表示相同事物的不同方法而已。quote(或者单引号)可以在任何地方使用,来阻止Clojure立即对一个表达式求值。实际上,它的作用远不 止于声明一个列表,当涉及到元编程的时候,单引号十分必须。这个后面在对符号作用进行详细说明。
clojure 新手指南(5):判断&基本类型 - climbdream的个人空间 - 开源中国社区
https://my.oschina.net/clopopo/blog/142314
判断语句
在Clojure中,所有的语法规则最终都是S表达式。我们如何知道哪些是判断语句呢?这个很简单,clojure中(lisp习惯)有个规定:对于判断功能的函数,函数名后面都有一个“?”号。所以我们只要看到后面带问号的函数名,就知道这一定是一个判断语句。很简单吧!
例如 "fn?"这个函数用于判断传入的参数是否是一个函数:
=>(fn? reduce)true=>(fn? 42)false
基本类型
数字(Number)
Clojure支持非常丰富的数字类型的数据。每一种数字类型都提供了不同的计算精度,当然也占用不同的内存空间。当我们选择不同的数据类型时,精度、内存消耗这些因素对计算的性能和准确度有着至关重要的影响。所以我们必须对不同的数据类型有着深入的了解。
=>4242;;整形=>(class 42)java.lang.Integer;;判断是否是数字=>(number? 42)true;;判断是否是整形=>(integer? 42)true=>21.4221.42;;查看类型=>(class 21.42)java.lang.Double;;判断是否是数字=>(number? 21.42)true;;判断是否是整形=>(integer? 21.42)false
整形和浮点型在其他语言中都是常见的数据类型。但是分数(ratios)这种就非常少见了。在Clojure中,你可以将二分之一写成1/2,当然也可以用0.5。
=>1/21/2=>(class 1/2)clojure.lang.Ratio=>(ratio? 1/2)true=>(* 1/2 1.0)0.5
使用ratios类型的好处:在进行数据计算时,特别是使用大量的除法时,我们可以使用分数形式。等到得到最终的结果后,我们在对分数求值。这样最大的减少精度损失。(一旦遇到无法整除的除法运算都很可能减少精度)。
字符(Character)
字符代表一个字母、一个数字、一个标点符号或者其他符号。在Clojure中,符号使用反斜杠“\”作为开始。
=>\C \C=>(class \C)java.lang.Character=>(char? \C)true
字符串 (String)
字符组成一起就是字符串。字符串使用双引号括起来(这招很通用)。
=>"some characters in a string""some characters in a string"=>(class "some characters in a string")java.lang.String=>(string? "some characters in a string")true=>(str \C)"C"
符号(Symbol)
符号被作为一种标示符。为了常用目的经常绑定到数据或者函数上。符号之前说过了,用单引号" ' "开头,或者使用quote函数。
=>'stuffstuff=> (quote stuff)stuff=>(class 'stuff)clojure.lang.Symbol=>(symbol? 'stuff)true
符号也可通过字符串来创建:=>(symbol "more stuff")more stuff=>(class (symbol "more stuff"))clojure.lang.Symbol
关键字(Keyword)
关键字是另一种类型的符号,它不适用于数据绑定的。关键字主要用于匹配。例如在哈希表中作为key,它比使用字符串作为key检索起来要快的多。关键字以冒号”:“开头。
=>:stuff:stuff=>(class :stuff)clojure.lang.Keyword =>(keyword? :stuff)true
关键字也可以通过字符串来创建:=>(keyword "stuff"):stuff=>(class (keyword "stuff"))clojure.lang.Keyword
布尔值(Boolean)
这个太直观了。就俩值:true 和 false
=>truetrue=>falsefalse=>(class true)java.lang.Boolean=>(class false)java.lang.Boolean
clojure 新手指南(6):全局绑定&匿名函数 - climbdream的个人空间 - 开源中国社区
https://my.oschina.net/clopopo/blog/142394
绑定变量
在Clojure中,我们可以使用 " def " 给函数和数据赋予一个名字。例如我们定义一个叫做“alphabet”的字符串变量
user=> (def alphabet "abcdefghijklmnopqrstuvwxyz") #'user/alphabet user=> alphabet "abcdefghigklmnopqrstuvwxyz"
使用变量的好处一个是方便使用,另一个在于一个有意义的名字大大增强代码的可读性。举个例子,如果我们想要知道一个字符串的包含的字符数量:
user=> (count alphabet) 26
我们就不需要在每次去计算字符串数量的时候都要写下长长的一大串了。只用把与该字符串绑定的变量名传过去就行。使用变量绑定,我们不仅能使代码具有良好的可读性,有时候还能增加代码性能。增加性能?这个很好理解也很常用。例如我们经常把计算好的值绑定到一个变量上。这样我们在用到这个值的时候就不用每次都得重新计算了。对于一些费时的操作,这可能省下大量的时间。下面的例子就说明了这一点:
user=> (def alphabet-length (count alphabet))user=> alphabet-length 26
如果我们要计算字符串长度的两倍,就可以直接使用绑定的字符串长度:
user=> (+ alphabet-length alphabet-length) 52
使用def绑定类似其他语言中的赋值操作。绑定匿名函数
首先我们看下面这个例子:
user=> (def alphabet "abcdefghijklmnopqrstuvwxyz") ;;将aplhabet绑定到一个字符串 #'user/alphabetuser=> (def alphabet-length (count apphabet)) ;;将alphabet-length与长度绑定 user=> alphabet ;;此时为26 26user=> (def alphabet "abc") ;;重新绑定到新的字符串user=> alphabet ;;依然是26,没有变化 26
上面例子中,我们改变alphabet后,alphabet-length是没有变化的。当然对于上面例子来说,没有变化是正常的。但是我们如何能让alphabet的变化能体现在alphabet-length上呢?这就需要函数绑定了。我们可以使用关键字 " fn " 来定义一个匿名函数。
user=> (fn[](count alphabet)) ;;这里定义了一个匿名函数 user$eval6036$fn__6037 user$eval6036$fn__6037@6e3ebbb0user=> (def alphabet-length (fn[](count alphabet))) ;;这里定义了一个同样的匿名函数,不过绑定到了alphabet-length上 #'user/alphabet-length
上面有一个注意点,函数中使用的alphabet是我们之前已经绑定好的。使用def的绑定属于全局绑定,就是说对于其他函数是可见的。在clojure中,函数总会有一个返回值,那就是函数体的执行能到达的最后一个表达式。所以上面定义的匿名函数最终会返回alphabet的长度。我们将匿名函数绑定到了alphabet-length上,每次我们输入alphabet的时候,都会去执行这个匿名函数。所以我们只要去改变alphabet的值,因为这种改变时全局的,alphabet-length都会重新计算成新的字符串长度。看下面例子:user=> alphabet-length ;;不执行函数,返回函数字面量 user$alphabet_length user$alphabet_length@71d408f7user=> (alphabet-length) ;;执行函数,返回函数执行结果 3
我们在使用绑定函数的时候要注意,此时alphabet-length是与函数绑定的,换句话说它代表的是一个函数。在clojure中,函数的调用必须要在S表达式中。如果直接使用函数名,会返回函数的字面量,并不会执行函数。我们再来看一下如何绑定需要传参的匿名函数。我们定义一个两个数字相加的匿名函数,并绑定到add上:
user=> (def add (fn [a,b](+ a b))) ;; fn后的[]内放置参数列表,多个参数用逗号分隔user=> (add 1 2) 3
绑定匿名函数的另一种形式
clojure提供了一种比较简洁的方式让我们来对匿名函数进行绑定
;; 将 (fn (count alphabet)) 替换成了#(count alphabet) ,省了几个括号user=> (def alphabet-length #(count alphabet)) #'user/alphabet-length
再看一下需要参数传递的例子(非常简洁);;这里的参数直接使用%,如果只有一个参数只用 "%"即可。多个参数是 "%[参数序号]"user=> (def add #(+ %1 %2))user=> (add 1 2) 3
clojure 新手指南(7):定义函数 - climbdream的个人空间 - 开源中国社区
https://my.oschina.net/clopopo/blog/142589
前几章中,我们用了一种比较迂回的方式创建函数:把匿名函数绑定到一个变量上。实际上,clojure提供了一个更好的方式做同一件事情。“defn” 这个函数就是专门用于定义函数的。
在我们使用defn之前,我们再回顾一下之前我们怎么使用def来创建函数的,然后我们使用defn来做同一件事情对比一下。
;;使用def=>(def alphabet-length (fn [ ](count alphabet)))#'user/alphabet-length=>(alphabet-length)26;;使用defn=>(defn alphabet-length [ ](count alphabet))#'user/alphabet-length=>(alphabet-length)26
上面两种方式做的都是同一件事情。但是defn能做更多的事情。下面是defn定义函数的一个脚手架:
[1] (defn name 函数名 [2] "description" 函数描述 (可选) [3] {metadata} 元数据 (可选) [4] [arguments] 参数列表 [5] body-of-expressions...) 函数体
上面我们可以看出,defn在定义函数时可以提供更多的信息。下面让我们用上面这些信息定义一个函数:
=>(defn select-random "从一个列表中随机返回一个元素" {:added "1.2"} ;; 元数据 [options] (nth options (rand-int (count options))))#'user/select-random
(count options) 用于计算options包含的元素数量。(nth options x) 用于从options中获取第x个元素(从0开始,类似java中的list的get方法)
我们之前说过clojure是lisp的一种方言。lisp 是 “List Processor”的缩写,就是列表解析的意思,使用列表来表示所有的东西(S表达式)。从我们写的代码也可以看出,整个代码结构就是一个嵌套的列表。现在让我们用列表结构来保存数据:
=>(list "growl" "lick" "jump")("growl" "lick" "jump")
我们之前定义的函数select-random需要的参数正是一个列表,正好我们就可以用来测试:=>(select-random (list "growl" "lick" "jump"))"jump"=>(select-random (list "growl" "lick" "jump"))"growl"
运行一切正常,说明select-random没什么问题。我们可以在一个新的函数中来使用它。我们来创建一个用于问候的函数greeting。=>(defn greeting "Composes a greeting sentence. Expects both the name of a greeter and the name of whom is to be greeted for arguments. An approach and an action are randomly selected." {:added "1.2"} [greeter whom] ;;str 用于组装字符串 (str greeter " greeted " whom " with a " (select-random (list "ferocious" "wimpy" "precarious" "subtle")) " " (select-random (list "growl" "lick" "jump")) "!")) #'user/greeting=>(greeting "Jon" "Thaddeus")"Jon greeted Thaddeus with a wimpy growl!"=>(greeting "Jon" "Thaddeus")"Jon greeted Thaddeus with a precarious lick!"
当然,上面的问候函数不是很完美。我们可以把问候语句单独提出来。
=>(def approaches (list "ferocious" "wimpy" "precarious" "subtle"))'user/approaches=>(def actions (list "growl" "lick" "jump"))#'user/actions
然后在greeting中使用绑定的列表:=>(defn greeting "Composes a greeting sentence. Expects both the name of a greeter and the name of whom is to be greeted for arguments. An approach and an action are randomly selected." {:added "1.2"} [greeter whom] (str greeter " greeted " whom " with a " (select-random approaches) " " (select-random actions) "!"))#'user/greeting
现在可读性好多了吧,把变化的部分单独抽象出来这个原则对于函数式编程也是通用的哦。这样我们就可以在不修改函数的情况下改变问候语句了。至于函数定义中的元数据有什么作用,暂时保密,后面会单独来讲。
clojure 新手指南(8):参数和重载 - climbdream的个人空间 - 开源中国社区
https://my.oschina.net/clopopo/blog/142705
现在我们首先定义一个支持4个参数相加的函数:
(defn add [ v1 v2 v3 v4] (+ v1 v2 (if v3 v3 0) (if v4 v4 0) ))
我们想达到这样一种效果。如果我们调用(add 1 2 3 4),则正常返回10。如果我们调用(add 1 2 3)能得到结果6。或者调用(add 1 2)能得到3。
我们执行后发现:
=> (add 1 2 3 4) 10=> (add 1 2)... ArityException Wrong number of args (2) passed to: user$add ...
当参数数量和我们定义函数时的参数数量一致时能得到正确的值。否则会抛出参数数量不匹配异常。看来这个和我们预想的不一样啊。
固定参数
我们之前定义函数时,使用的都是固定参数方式。调用函数时,只要我们传入参数的数量不符合定义时的参数数量,clojure都会提出抗议(抛出参数数量不匹配异常)。像上面我们可以这么调用(add 1 2 3 nil),这样会返回6。 (if v4 v4 0)的含义是,如果v4 不为nil或false,就返回v4,否则返回0。我们传入的正是nil。所以函数会正确返回6。当然(add 1 2 nil nil )也会正确返回3。
难道clojure就这么点能力?当然不是,clojure的固定参数可不止这点能耐。我们在上篇列出的定义函数时的脚手架其实并没有列完整。请看下面一个更加完整的脚手架:
(defn variable-name "description" {metadata} ([argument-pattern #1] ;;第一种参数形式 body-of-expressions) ([argument-pattern #2] ;;第二种参数形式 body-of-expressions) more-patterns... )
先让我们用上面方式重构一下之前的add函数,看看怎么来支持2个、3个或4个参数的相加:
(defn add ( [v1 v2] ( + v1 v2)) ( [v1 v2 v3] (+ v1 v2 v3)) ( [v1 v2 v3 v4] (+ v1 v2 v3 v4)))
上面我们总共定义了三种不同参数形式的add函数。这样调用的话(add 1 2)会匹配第一种形式,正确返回3。(add 1 2 3)会匹配第二种参数模式,返回结果6。这其实就是lisp中模式匹配的一种应用。是不是比java中的重载方式更加灵活呀。这只是固定参数的一种用法,下面再来看看clojure的可变参数用法。
可变参数
如果我们的参数模式就两三种(例如上面只需要支持3种不同个数的数字相加),我们可以采用固定参数+模式匹配来实现函数。但是如果我们的参数模式个数不固定(例如支持任意个数字相加),固定参数+模式匹配也拯救不了我们了。不过不用担心,clojure还给我们提供了可变参数 。
我们看一下如何使用可变参数来实现任意个数的数字相加:
(defn add [v1 v2 & others] ;;&后面的是可变参数 (+ v1 v2 (if others ;;判断可变参数列表是否是空,如果不是累加列表中的值,否则返回0 (reduce + 0 others) ;;使用reduce函数计算others的数字之和。 0 ) ))
这里有几点需要注意一下,固定参数要写在一个“&”之后,只能有一个可变参数。&后的可变参数名代表的是由可变参数组成的列表。这里的reduce函数,先理解成对列表中的数字进行累加就行,本文目的主要是理解可变参数的用法。
现在我们执行一下上面定义的函数:
=> (add 1 2) 3=> (add 1 2 3) 6=> (add 1 2 3 4 5 6) 21
现在add已经支持任意个数字参数相加了。
clojure 新手指南(9):元数据 - climbdream的个人空间 - 开源中国社区
https://my.oschina.net/clopopo/blog/142982
我们在定义函数的时候提到了如何去定义一个元数据。但之前只是定义它,并没有明说它的用途。让我们再看一下之前定义的select-random函数,我们添加了一个叫做:add的元数据。注意:元数据是以哈希表形式展现的。(键和值可以是任何类型,不过key一般推荐为关键字类型)
=>(defn select-random "从一个列表中随机返回一个元素" {:added "1.2"} ;; 元数据 [options] (nth options (rand-int (count options))))#'user/select-random
我们可以使用下面方式去查看一个函数的元数据信息(一个哈希表):
=>(meta #'select-random){:ns @<Namespace user>@, :name select-random, :file "NO_SOURCE_PATH", :line 1, :arglists ([options]), :added "1.2", :doc "从一个列表中随机返回一个元素"}
我们虽然只定义了一个元数据:add,但是系统却给我们返回了一堆元数据。这些元数据是系统默认给函数添加了,主要是函数的一些基本信息。下面是一些比较重要的信息:
:ns 命名空间
:name 函数名
:file 对应的源码文件
:arglists 参数列表 (一个函数刻意包含多个参数列表(见上篇),所以是lists 而不是list)
:doc 函数描述
下面是一些元数据的使用场合:
1、定义函数时,可以添加对应的clojure的版本。这样一旦clojure升级,你可以系统的测试任何相关的函数。
2、做一些类似java注解方面的工作。例如,如果函数已不再使用,可以添加:state "deprecated"。
3、给函数添加一些统计信息等等。
我们可不仅限于只给函数添加元数据。任何能绑定变量的都可以添加元数据,例如符号或者其他数据结构。
=>(def approaches (with-meta (list "ferocious" "wimpy" "precarious") {:creator "tim"}))#'user/approaches=>(meta approaches){:creator "tim"}
clojure 新手指南(10):与java交互 - climbdream的个人空间 - 开源中国社区
https://my.oschina.net/clopopo/blog/143000
clojure是基于jvm的语言,就是说clojure会被编译成字节码被jvm执行。但是clojure能做的可不仅仅是被编译成字节码,它提供了一套API让用户与java交互。因此clojure可以直接调用java世界中那些丰富庞大的优秀库了。
数据&转换
=>12.5612.56
在clojure中,我们使用数据时似乎并没有像其他语言那样需要一些特殊的处理或者声明。但是,clojure在底层实际创建了java对象,并指定了对应的java类型。我们可以通过class函数来看一下编译器为我们创建的数据的类型。=>(class 12.56)java.lang.Double
上面我们可以看出,clojure自动为我们创建了一个Double类型的java对象。如果我们想在clojure指定对应的java类型,可以这么做:
=>(new java.lang.Float 12.56)12.56=>(class (new java.lang.Float 12.56))java.lang.Float
new 也是一个函数,我们使用它创建了一个Float类型的对象,貌似比java还麻烦。clojure给我们提供了一个更简洁的语法来做同样的事情。我们在对应的构造函数名字后加一个点,然后后面依次写上构造函数需要的参数即可。=>(Float. "12.56")12.56=>(class (Float. "12.56"))java.lang.Float
这里 Float. 就是Float的构造函数名加上一个点。这可不是clojure的函数调用。我们可以使用fn?测试一下,Floag.并不是函数。应该只是clojure的特殊的语法调用吧。下面是一些将字符串转换为数字的例子:
=>(+ "12.56" "5.92")java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Number=>(class "12.56")java.lang.String=>(+ (Float. "12.56")(Float. "5.92"))18.48=>(+ (Float. "12.5648230948234")(Float. "5.92"))18.484823=>(+ (Double. "12.5648230948234")(Double. "5.92"))18.484823094823398
日期和时间函数
clojure本身并没有专门的日期和时间处理函数。不过不用担心,java有的功能,clojure基本上都可以去调用。而且一旦我们用clojure函数将java的方法包装后,我们后面的调用就再也不会和java打交道了。
下面就看看如何使用java接口来创造我们自己的日期时间函数。让我们从调用系统当前时间开始吧。这一次我们先采用一种“斜线调用”形式。我们使用斜线一次隔开类名和调用的静态方法名称。我们将要调用java中的** **System.currentTimeMillis()方法来获取当前系统时间的毫秒数。
=>(defn msec [] ;;使用函数msec包装 (System/currentTimeMillis)) ;;这里的斜线相当于java中的 "."#'user/msec=>(msec) ;;调用msec,返回当前系统时间毫秒数 1307328017335=>(msec)1307662973116=>(> 1307328017335 1307662973116)false=>(> 1307662973116 1307328017335)true
上面是静态方法调用的展示,下面再来看看如果调用实例方法。让我们创建两个java.util.Date对象。注意,之前我们使用的都是java.lang下面的类,所以不需要手动导入。非lang包下的必须手动导入(和java一样一样的)。
=>(Date.) ;;导入前,找不到Date类java.lang.IllegalArgumentException: Unable to resolve classname: Date...=>(import java.util.Date) ;;手动导入Date (语法都和java一样)java.util.Date =>(Date.) ;;再次创建Date对象 #<Date Sun Jun 05 20:48:19 MDT 2011>=>(class (Date.)) ;;查看一下对象类型,确实是 java.util.Datejava.util.Date
现在我们来用clojure来包装一下,我们创造一个名为date的函数。该函数有两种参数形式:无参数和接受毫秒值(对应Date 的无参构造函数和 Date(long time)构造函数)。
=>(defn date ;;使用函数date封装java API () ;;第一种是无参数模式 ([systime](Date. systime))) ;;第二种是接受一个参数#'user/date=>(date) ;;调用date函数,无参数#<Date Sun Jun 05 20:41:42 MDT 2011>=>(date 1307328017335) ;;调用date函数,传入一个long值#<Date Sun Jun 05 20:40:17 MDT 2011>
为了让我们时间处理在强大点,我们在利用java库中的 SimpleDateFormat类将时间转换成特定形式的字符串。
=>(import java.text.SimpleDateFormat) ;;导入需要的类java.text.SimpleDateFormat=>(defn format-date ;;使用自定义函数封装 ([](format-date (date) "yyyy MM dd HH mm ss")) ([x](if (string? x) (format-date (date) x) (format-date x "yyyy MM dd HH mm ss"))) ([dt fmt](.format (SimpleDateFormat. fmt) dt)))#'user/format-date=>(format-date)"2011 06 04 17 50 21"=>(format-date (date 404534000000))"1982 10 26 20 33 20"=>(format-date "yyyy/MM/dd HH:mm:ss")"2011/06/04 17:51:00"=>(format-date (date 404534000000) "yyyy/MM/dd HH:mm:ss")"1982/10/26 20:33:20"
其他没有什么难点,都是之前说过的内容,我们主要解释上面代码中下面这一句的意义:
([dt fmt](.format (SimpleDateFormat. fmt) dt)))
[dt fmt]是参数列表,这个没什么特别的东西。主要看看函数体。 .format 是方法名 (SimpleDateFormat. fmt)最终返回的是 SimpleDateFormat类型的对象(相当于调用 new SimpleDateFormat(fmt)) ,dt 是参数所以整个意思是调用实例 (SimpleDateFormat. fmt)的format 方法,并传入参数dt。翻译成java代码就是:(new SimpleDateFormat(fmt)).format(dt)
很简单吧。个人觉得使用clojure封装后的java代码比原本的java API更要紧凑和灵活。这主要依靠了clojure这种动态语言+函数式语言的N多优势。
clojure 新手指南(11):正则表达式 - climbdream的个人空间 - 开源中国社区
https://my.oschina.net/clopopo/blog/143294
接着上篇,继续我们的时间和日期函数的探讨。我们可以定义一个函数,将一个日期字符串分成一个列表。列表元素分别为年、月、日、时、分、秒。为了完成这个字符串分割操作,我们使用“re-split‘函数。re-split函数需要一个正则表达式参数用于确定如何分割字符串。Clojure 依赖java的正则表达式库来处理这些操作。
re-split函数是Clojure Contrib中字符串库的一部分,所以这就需要确保你能访问Clojure Contrib库。你可以用下面这种方式来加载字符串工具库。(注意,记得进入REPL时要加载contrib.jar,忘了点这 )
=> (use 'clojure.contrib.str-utils)nil
一旦加载完库,我们就可以使用re-split了:
=>(re-split #" " "2011 06 04 17 50 21")("2011" "06" "04" "17" "50" "21")=>(class #" ")java.util.regex.Pattern=>(re-split #":" "2011:06:04:17:50:21")("2011" "06" "04" "17" "50" "21")
我们上面用到的正则表达式非常直白。它们以”#“开头,后面跟着包含需要匹配的正则模式。正则表达式非常复杂,不是本文重点,这里只是讲解clojure的相关用法。
让我们随便看几个例子:
=>(re-split #":" "2011:06:04:17:50:21")("2011" "06" "04" "17" "50" "21")
方括号里代表符合其中一个即可:
=>(re-split #"[/:]" "2011/06/04 17:51:00")("2011" "06" "04 17" "51" "00")
最后我们尝试构建自己的日期元素列表函数date-list
=>(defn date-list ([](re-split #"\W+" (format-date))) ([systime](re-split #"\W+" (format-date systime))))=>(date-list)("2011" "06" "05" "11" "21" "21")
本章没什么新内容,主要难点就在正则表达式上。不过这个和clojure没什么关系了。
clojure 新手指南(12):本地绑定&词法作用域 - climbdream的个人空间 - 开源中国社区
https://my.oschina.net/clopopo/blog/143803
如果你还没忘的话,仔细回想一下,之前我们是如何将对象绑定到变量名上的。但当时我们只是全局绑定,在那时这种绑定是非常有用的。不过,有很多时候,本地绑定往往比全局绑定更合适,例如把变量限制在一个操作内部的时候。下面就让我们看看如果使用绑定函数 "let " 进行本地绑定。
=>idjava.lang.Exception: Unable to resolve symbol: id...=>(let [id 1] (println id))1nil=>idjava.lang.Exception: Unable to resolve symbol: id...
正向你看到的南阳,通过使用"let" 操作,我们把1绑定到了”id“这个变量名上。然后我们又把它打印了出来。当这个操作执行完后,我们在外面查看”id“时却是无法解析的。这就证明了变量”id“只存在于操作内部(本地绑定类似于java中的方法局部变量)。
本地绑定可以将变量限制在某个操纵内,这样就不会造成对其他操作的变量污染。试想一下,如果没有本地绑定,一旦我们使用了id这个变量名后,我们就再也不能使用它来绑定其他对象了。使用本地绑定后,我们可以在某个操作内使用任何有意义的变量名而不用担心和其他相同名字的变量造成冲突,即使是全局变量:
=>(def id 0) ;;全局绑定#'user/id=>id0=>(let [id 1] ;;本地绑定 (println id))1nil=>id ;;本地绑定对全局绑定没有任何影响0
这种行为通常被称为词法作用域,我们可以保护变量不受污染,甚至是父操作也存在同样的变量名:
=>(let [id 1] ;;外层操作 (let [id 2] ;;内层操作 (println id)) ;;内层本地绑定的id (println id)) ;;外层本地绑定的id 21nil
我们再来举一个例子。上一篇最后我们写了一个”data-list"函数,这个函数最终返回给我们一个包含各个时间元素的列表。我们每调用一次,它都会返回给我们当前时间年、月、日、时、分、秒组成的一个列表:
=>(date-list)("2013" "07" "10" "15" "02" "59")=>(date-list)("2013" "07" "10" "15" "03" "02")
现在呢,我们想要这么一个函数 run-report,通过这个函数,我们能只打印出"时"和"分"这两个元素。这个简单我们可以向下面这样去实现它:
=>(defn run-report [] (str "report ran: " (nth (date-list) 3) ":" (nth (date-list) 4)))#'user/run-report=>(run-report)"report ran: 15:04"
上面这个函数有什么问题吗?聪明的你就会发现data-list这个函数被调用了两次。一次我们用来获取小时,一次我们用来获取分钟。这样做的话有两个坏处。第一个是,这两次调用返回的时间是不一样的(函数在快也需要时间执行),我们很可能得到非常错误的结果。假如第一次调用恰好是15:59:59,到了接近16点的临界点。第二次调用变成了16:00:00。这两个组合就变成了 15:00 。第二个坏处就是,排除第一个错误的话,如果data-list执行时间比较长,多次调用势必影响函数效率。
更好的做法就是我们只调用一次data-list,然后把调用后的结果绑定到一个本地变量上:
=>(defn run-report [ ] (let [date (date-list)] ;;data-list是全局变量。date是本地变量 (str "report ran " (nth date 3) ":" (nth date 4))))#'user/run-report=>(run-report)"report ran: 15:09"
通过上面的做法,之前的两个问题都不复存在了。
下面是另一种做法:
=>(defn run-report [date] (str "report ran: " (nth date 3) ":" (nth date 4)))#'user/run-report=>(run-report (date-list)) ;; 传入参数就是一种隐式的本地绑定"report ran: 15:10"
我们给run-raport 函数添加了一个参数date,参数对函数来说就是一个隐式的本地绑定。当函数被执行时,用实参替换形参的时候,本地绑定就自动的和隐式的进行了。
关于词法作用域可以参考这篇文章,虽然是关于javascript的,但道理是一样的。
clojure 新手指南(13):序列&向量 - climbdream的个人空间 - 开源中国社区
https://my.oschina.net/clopopo/blog/144421
序列
我们知道clojure是Lisp的一种方言,那么这也意味着对这门语言必然植根于“列表解析”。但是在Clojure中,我们优先使用"序列"来创造列表和管理列表中的元素。
列表
之前我们说过,Lisp系列语言整个都建立在列表之上。我们使用"list"函数来创建一个列表,但后面你就会发现创建列表的方式不只一种。如果你不想让你列表中的元素被解释执行,记得引用(quote)一下。
=> (list "truck" "car" "bicycle" "plane") ;;创建一个列表 ("truck" "car" "bicycle" "plane");;与上面方式等价=> '("truck" "car" "bicycle" "plane") ("truck" "car" "bicycle" "plane");;查看列表的类型=>(class '("truck" "car" "bicycle" "plane"))clojure.lang.PersistentList;; 给创建的列表绑定一个全局变量 => (def vehicles (list "truck" "car" "bicycle" "plane"))#'user/vehicles;;查看是否是序列=>(seq? vehicles)true;;查看是否是列表 =>(list? vehicles)true;;查看是否是集合 => (coll? vehicles)true;;获取第一个元素=>(first vehicles)"truck";;获取第二个元素 =>(second vehicles)"car";;获取最后一个元素=>(last vehicles)"plane";;获取除第一个元素以外的剩下列表=>(rest vehicles)("car" "bicycle" "plane");;获取第n个元素=>(nth vehicles 0)"truck"=>(nth vehicles 1)"car";;添加元素 (这只是返回一个新的列表,vehicles 并不会被改变)=> (conj vehicles "motorcycles")("motorcycles" "truck" "car" "bicycle" "plane")
Cons
Cons是lisp语言中另一个类似列表的一种数据结构。术语”cons“意思就是构造一个对(pair),将这些对链接在一起然后形成一个类似列表的数据结构。就像list一样,cons既是一个类型,也是一个函数,我们可以使用cons来创建这种数据结构。
=>(cons "truck" (list "car" "bicycle" "plane"))("truck" "car" "bicycle" "plane")=>(class (cons "truck" (list "car" "bicycle" "plane")))clojure.lang.Cons=>(def vehicles (cons "truck" (list "car" "bicycle" "plane")))#'user/vehicles=>(seq? vehicles)true=>(list? vehicles)false=>(coll? vehicles)true=>(conj vehicles "motorcycle")("motorcycle" "truck" "car" "bicycle" "plane")=>(class (conj vehicles "motorcycle"))clojure.lang.Cons=>(cons "motorcycle" vehicles)("motorcycle" "truck" "car" "bicycle" "plane")=>(class (cons vehicles "motorcycle"))clojure.lang.Cons=>(conj "truck" nil)java.lang.ClassCastException: cannot be cast to clojure.lang.IPersistentCollection=>(cons "truck" nil)("truck")=>(class (cons "truck" nil))clojure.lang.PersistentList
注意最后一个例子,是不是看起来很奇怪?当我们使用cons将一个元素附加到一个列表或者另一个cons结构上时,返回的仍然是一个cons类型结构。但是当我们将一个item附加nil上时,返回的却是list类型。这一点尤其注意。
向量
向量是除了list和cons之外的另一个很受欢迎的数据结构,因为它有时用起来有一些独特的优势。举个例子,因为向量使用方括号来表示,所以至少从视觉上来说会让它从大量的圆括号中脱颖而出,提供了更好的可读性。另外使用向量通常能提供比列表更好的性能优势。
=>(vector "truck" "car" "bicycle" "plane")["truck" "car" "bicycle" "plane"]
;;一种简便的创建向量方式,这个不需要“引用”了哦=>["truck" "car" "bicycle" "plane"]["truck" "car" "bicycle" "plane"]=>(class ["truck" "car" "bicycle" "plane"])clojure.lang.PersistentVector=>(def vehicles ["truck" "car" "bicycle" "plane"])#'user/vehicles=>(seq? vehicles)false=>(list? vehicles)false=>(vector? vehicles)true=>(coll? vehicles)true
注意:虽然大多数函数对待向量和列表都会产生相同的结果,但有时候这种假设往往会引入一些问题。看下面例子,注意二者之间的区别:=> (conj ["a" "b"] "c")["a" "b" "c"]=> (conj '("a" "b") "c")("c" "a" "b")
clojure 新手指南(14):Hash-Maps ,Array-Maps & Sorted... - climbdream的个人空间 - 开源中国社区
https://my.oschina.net/clopopo/blog/145319
hash-map
创建
在clojure中,哈希表是最通用的一种Map,和java中的HashMap一样,它们在处理大量数据方面效率非常高,但是不保证顺序。我们可以使用函数hash-map来创建哈希表:
=>(hash-map :truck "Toyota" :car "Subaru" :plane "de Havilland"){:plane "de Havilland", :truck "Toyota", :car "Subaru"}=>(class (hash-map :truck "Toyota" :car "Subaru" :plane "de Havilland"))clojure.lang.PersistentHashMap
在clojure中,所有的类型对象都可以作为map的键,甚至是函数对象也可以。但是我们推荐使用关键字类型(以冒号开头)作为map的键,因为关键字作为键时哈希表性能最好。创建哈希表时不一定非得用hash-map 函数,下面就是一个更简便的方式:
;;直接使用{}来创建哈希表=>{:truck "Toyota" :car "Subaru" :plane "de Havilland"}{:truck "Toyota", :car "Subaru", :plane "de Havilland"}
不过,用上面这种方式,如果传入的键值对少于9个,它实际上创建的是ArrayMap而不是HashMap:
=>(class {:truck "Toyota" :car "Subaru" :plane "de Havilland"})clojure.lang.PersistentArrayMap
一旦你把它绑定到一个变量上,它自动就转换成了hashMap:
=> (def map {test: 1})#'user/map=> (class map)clojure.lang.PersistentHashMap
ArrayMap 和 HashMap这种转换,看起来很诡异。但是不要担心,因为大部分情况下,这都不会引起问题。我见过的所有函数对这两种map的操作都是一样的,没有任何区别。
创建map时,如果你想让键值对之间的分隔更清晰,可以使用逗号分隔符:
;;使用hash-map函数=> (hash-map :key1 1 , :key2 2){:key2 2, :key1 1};;直接创建=>{:key1 1 , :key2 2}{:key2 2, :key1 1}
我们再来看看和map相关的操作
读取
;;如果是map的键是关键字类型,关键字直接可以当做函数使用=> (def m {:key1 1, :key2 2}){:key1 1, :key2 2};;获取:key1对应的值=> (:key1 m)1;;map本身也可以用于取值,这种适应于任意类型的key=> (m :key1)1
一种更好的方式是使用get函数,因为可以设置缺省值。(get 也可以用于向量,把key变成索引值即可)
=> (def m {:key1 1 :key2 :2});;获取 :key1对应的值=> (get m :key1)1;;如果 :key3不存在,返回缺省值=> (get m :key3 "default")"default"
我们可以使用get-in 获取嵌套map的值
;;创建嵌套mapuser=> (def m {:username "sally" :profile {:name "Sally Clojurian" :address {:city "Austin" :state "TX"}}})
'user/muser=> (get-in m [:profile :name])"Sally Clojurian"user=> (get-in m [:profile :address :city])"Austin"user=> (get-in m [:profile :address :zip-code])nil;;如果键不存在,可以设置默认值 user=> (get-in m [:profile :address :zip-code] "no zip code!")"no zip code!"
我敢说这是json好么!!!(get-in 函数可不只是用于map,向量也可以使用get-in操作)
增加/修改
;;没则增加,有则修改 =>(conj {:name "qh" :age 20} {:age 30} {:gender 'male}){:gender mail, :age 30, :name "qh"};;没则增加,有则修改=>(merge {:name "qh" :age 20} {:age 30} {:gender 'male}){:gender mail, :age 30, :name "qh"};;assoc是操作map和对应的元素,上面两个操作的是多个map,注意区别=>(assoc {:name "qh" :age 20} :age 30 :gender 'male){:gender mail, :age 30, :name "qh"}
删除
;;删除:name对应的键值对=>(dissoc {:name "qh" :age 30} :name){:age 30};;给m绑定一个map=> (def m {:name "qh" :age 30})#'user/m;;执行删除操作=>(dissoc m :name){:age 30};;m没有变化=>m{:name "qh" :age 30}
我们的删除操作只是返回一个新的map,并不会对原有的map造成影响。这点要注意。这也是函数式编程中强调的"消除副作用"。之前的添加和修改都是如此。
获取所有的key
;;使用keys函数=> (keys {:name "clo" :age 30})(:name :age)
获取所有的value
;;使用vals函数获取所有value=> (vals {:name "clo" , :age 30})("clo" 30)
Sorted Maps
如果我们想要创建一个有序的map(按照key的自然顺序),可以使用sorted-map函数来创建一个有序map。
user=> (def sm (sorted-map :c 1 :b 2 :f 3 :a 3))#'user/smuser=> sm{:a 3, :b 2, :c 1, :f 3}
有序map增删改查操作和上面一样。
clojure 新手指南(15):可变性 - climbdream的个人空间 - 开源中国社区
https://my.oschina.net/clopopo/blog/148993
我们已经知道如何把数据绑定到一个变量上,这给我们提供了一种可共享的数据的持久化存储方式(数据被绑定到一个变量后,我们是无法对数据本身进行修改的,重新绑定又是另一回事了,和修改数据本身无关)
;;将列表绑定到lat上user=> (def lat (list 1 2 3))#'user/latuser=> lat(1 2 3);;我们得到的是一个新的列表user=> (cons 3 lat)(3 1 2 3);;原来列表并没有改变user=> lat(1 2 3)
但是,有时候我们确实需要在数据被共享的时候去修改它。
事物型引用(Transactional References)
Clojure 提供了接口用于协调一个对象的并发修改。通过事务型引用(在clojure中对应着Ref类型),clojure相当于创建了一个管卡,每次只允许一个事务(类似关系数据库中的事务概念)通过这个关卡,并且一个事务中的所有改变要么同时生效,要么回滚(这就是clojure软件事务内存STM的概念)。
包装引用对象
我们可以使用ref函数讲一个普通对象包装成Ref类型对象
;;包装一个空的哈希表=>(ref (hash-map))#<Ref@52879daa: {}>;;绑定一个Ref类型对象,该Ref包装了一个哈希表=>(def vehicles (ref {:truck "Toyota" :car "Subaru" :plane "de Havilland"}))#'user/vehicles=>vehicles#<Ref@14325ad8: {:truck "Toyota", :car "Subaru", :plane "de Havilland"}>
解引用对象
接上面的例子,我们已经有了一个被包装在Ref类型之下的一个对象vehicles,但有很多函数需要访问被包装的对象,而不是这个引用对象:
;; vehicles 不是一个map,所以会报错=>(keys vehicles)java.lang.IllegalArgumentException: Don't know how to create ISeq from: clojure.lang.Ref =>(map? vehicles)false;;vehicles是Ref类型对象=>(class vehicles)clojure.lang.Ref
我们可以使用defref函数来获取封装在Ref内部的对象,当然还有一种更简洁的方式就是使用@符号:=>(deref vehicles){:truck "Toyota", :car "Subaru", :plane "de Havilland"}=>@vehicles{:truck "Toyota", :car "Subaru", :plane "de Havilland"}=>(map? @vehicles)true=>(keys @vehicles)(:truck :car :plane)=>(vals @vehicles)("Toyota" "Subaru" "de Havilland")
修改引用对象
我们引用对象的目的,就是想修改它。之所以叫作事务型引用,是因为我们必须在一个事务中去修改它。在事务中(例如dosync)使用alter函数是最安全的修改方式,它能确保我们在事务操作期间,其他对这个对象的改变都不会发生。
;;使用dosync可以理解成开启了一个事务=>(dosync (alter vehicles assoc :car "Volkswagon")){:truck "Toyota", :car "Volkswagon", :plane "de Havilland"};;vehicles是真的被改变了,而不是返回一个新的对象=>@vehicles{:truck "Toyota", :car "Volkswagon", :plane "de Havilland"};;使用alter删除map的一个键值对=>(dosync (alter vehicles dissoc :car)){:truck "Toyota", :plane "de Havilland"};;修改在当前对象上生效了=>@vehicles{:truck "Toyota", :plane "de Havilland"}
如果你不关心引用对象原来的值的话,可以使用ref-set来设置一个新的值
=>(dosync (ref-set vehicles {:motorcycle "Ducati"})){:motorcycle "Ducati"}=>vehicles#<Ref@229ec9cd: {:motorcycle "Ducati"}>
原子类型(Atoms)
和引用类型(Ref)类似,原子也是创建一个管卡来修改一个不可变对象,并且原子也能和Ref一样进行同步更新。但是原子并不要求在事务中运行,并且它们不能协调多个状态的更新。(多个Ref类型对象可以在一个事务中协调更新,要么同时成功,要么同时失败,即回滚)
包装原子对象
操作和ref类似
;;绑定一个原子对象,该原子对象包装了一个哈希表=>(def vehicles (atom ["Toyota Tacoma" "Subaru Outback" "de Havilland Beaver"]))#'user/vehicles=>vehicles#<Atom@31f3f16b: ["Toyota Tacoma" "Subaru Outback" "de Havilland Beaver"]>
解引用原子对象
这个和ref一样
=>(deref vehicles)["Toyota Tacoma" "Subaru Outback" "de Havilland Beaver"]=>@vehicles["Toyota Tacoma" "Subaru Outback" "de Havilland Beaver"]
修改原子对象
我们可以使用swap!函数或者reset!函数(名字后面有感叹号在lisp方言中代表修改函数之意)来修改被原子包装的对象。swap!用于在原来值的基础上进行修改,reset!则是直接用新值替换原来的值。
=>(swap! vehicles conj "Ducati Diavel")["Toyota Tacoma" "Subaru Outback" "de Havilland Beaver" "Ducati Diavel"]=>@vehicles["Toyota Tacoma" "Subaru Outback" "de Havilland Beaver" "Ducati Diavel"]=>(reset! vehicles (take 2 @vehicles))("Toyota Tacoma" "Subaru Outback")=>@vehicles("Toyota Tacoma" "Subaru Outback")
clojure 新手指南(16):基本迭代&递归 - climbdream的个人空间 - 开源中国社区
https://my.oschina.net/clopopo/blog/149922
迭代和递归是两种不同的概念,但是它们彼此之间又有点相似。迭代是遍历一组元素,并在遍历的过程中对每一个元素做相应的操作,递归则是执行一个自己调用自己的操作。
从递归和迭代的概念上来看,这完全是两种完全不同的东西,那么它们的相似性又体现在什么地方呢?首先递归也可以作为一种遍历集合元素的方法,Clojure就有递归方式的迭代器。本章就是揭示clojure中迭代和递归的工作方式和使用它们的好处。
使用doseq进行迭代
首先让我们看一个示例问题,我们最终需要使用迭代来解决这个问题。我们的这个示例问题被称为FizzBuzz难题:
写一个程序打印1到100这些数字。但是遇到数字为3的倍数的时候,打印“Fizz”替代数字,5的倍数用“Buzz”代替,既是3的倍数又是5的倍数打印“FizzBuzz
让我们开始解决吧!从题目中分析知道,首先我们要确定1到100这些数字中可以整除3、整除5、既能整除3又能整除5的数字,所以至少需要一个判断整除的函数,我们不妨称之为multiple?(以问号结尾的函数名一般都返回布尔值)。我们可以利用clojure的内置取余函数mod来创建我们的multiple?函数。
=>(defn multiple? [n div] ;; n 除以 div的余数是否等于0 (= 0 (mod n div)))#'user/multiple;; 3能被3整除,返回true=>(multiple? 3 3)true;;判断4能被3整除,返回false=>(multiple? 4 3)false;;判断5能被3整除,返回false=>(multiple? 5 3)false;;判断6能被3整除,返回true=>(multiple? 6 3)true
现在我们已经有了一个判断整除的函数multiple?,可以开始着手处理FizzBuzz问题具体的处理了。在Clojure世界中,有多种方式可以遍历元素。下面,我们将会使用'doseq'( 宏标签) 来做迭代操作。它会遍历序列中的元素,并在遍历过程中做相应的处理。我们给doseq的第一个参数应该是一个向量(vector),这个向量里包含一个绑定当前元素的变量名(我们下面使用字母 i)和被遍历的序列。doseq的第二个参数是一个操作表达式(s表达式),遍历过程中将对每一个元素做处理。
先看一下一个简单的例子:
;;打印0到9的数字user=> (doseq [i (range 0 10)] (println i))0123456789nil
再来看看嵌套迭代(类似嵌套for循环)
user=> (doseq [ x [1 2 3] y [1 2 3]] (println (* x y)))123246369nil
上面代码和下面java代码基本等价(所有的clojure表达式都是有返回值的,上面代码中最后的nil就是返回值):
int [] array = {1, 2, 3};for(int i : array){ for(int j : array){ System.out.println( i * j ); }}
doseq介绍到此结束,我们来看如何使用doseq来解决我们的FizzBuzz问题:=>(doseq [i (range 1 101)] ;;遍历1到100 ;;首先判断是否能同时被5和3整除 (cond (and (multiple? i 3)(multiple? i 5)) (println "FizzBuzz") ;;如果上面不满足则判断是否能被3整除 (multiple? i 3) (println "Fizz") ;;如果上面不满足则判断能否被5整除 (multiple? i 5) (println "Buzz") ;;否则直接打印数字值 :else (println i)))12Fizz4BuzzFizz78FizzBuzz11Fizz1314FizzBuzz省略.......
强调一点:cond 的使用和if else非常像,cond按照从上到下的顺序依次判断表达式的真值,如果条件表达式真值为true,返回该条件表达式对应的执行表达式的值,然后此次判断结束,否则会执行下一条判断语句,直至最终执行到else语句。
;;cond 形式如下(cond (条件表达式1) (执行表达式1) (条件表达式2) (执行表达式2) ...... :else (执行表达式n)) ;;cond 结束
使用for进行迭代
for循环是另一种迭代的方式,但是接下来你会发现使用for循环不适合解决FizzBuzz问题。for循环的语法和doseq是一样的,只不过for 返回lazy seq(类似python 中的yield)而doseq是side effect。这么说有点抽象,还是用例子来说明吧:
;; 我们原想返回0-10中所有的偶数,但是得到的结果是niluser=> (doseq [x (range 0 11) :when (even? x)] x)nil ;; 使用doseq只能返回nil,不够我们可以在遍历期间做其他事情。比如 打印user=> (doseq [x (range 0 10) :when (even? x)] (print x ","))0 ,2 ,4 ,6 ,8 ,nil ;; (nil 是整个式子的返回值,不要搞混了);;我们使用for来获取0-10中所有的偶数user=> (for [x (range 0 10) :when (even? x)] x)(0 2 4 6 8)
可以这么说,使用doseq就向java中的for循环,只能在循环过程中做些什么事情,而clojure中的for循环可以在每次的遍历中向外输出值,最终由这些值组成一个序列。
再用个例子体会一下
user=> (for [x [0 1 2 3 4 5] :let [y (* x 3)] :when (even? y)] y)(0 6 12) ;;我们得到的结果
for循环不适合解决FizzBuzz问题的原因就在于,FizzBuzz只是在遍历过程中需要打印出对应的值,而不需要每次都返回结果。有兴趣你可以把解决FizzBuzz代码中的doseq换成for来看看输出效果就明白了。
使用loop进行递归
loop 在许多语言中都有这个关键字,基本上都是为了更好的使用迭代器而存在。但是在Clojure中,loop实际上是递归的,所以使用它需要更多一点的相关知识和代码。
先看一下如何使用loop 来解决 FizzBuzz问题,体会一下
(loop [data (range 1 101)] (if (not (empty? data)) (let [n (first data)] (cond (and (multiple? n 3)(multiple? n 5)) (println "FizzBuzz") (multiple? n 3) (println "Fizz") (multiple? n 5) (println "Buzz") :else (println n)) (recur (rest data)))))
首先cond里面的逻辑和之前doseq的一模一样,这个是不变的。我们知道递归必须有一个结束条件,所以我们在这里在递归开始加入了一个判断语句(if (not (empty? data)) ,就是判断data是否为空列表,如果为空递归结束,否则继续进行。每次递归,我们都从列表中取出一个值,然后把它传递给cond那部分逻辑进行判断。cond逻辑结束后,为了能递归调用上面逻辑,我们使用recur来达到目的。上例中,我们每次都将使用本次递归中的列表除第一个元素以外的剩下列表进行下一次递归。(递归必须是一个收敛的过程,否则递归将永远无法结束)我们使用loop来打印0-11的偶数,对比之前的例子。主要体会如何使用递归思想来解决问题
user=> (loop [x 0](when (<= x 10) ;;判断递归是否结束的语句 (if (even? x) (println x)) (recur (+ x 1)))) ;;使用recur 向判断结束方向收敛
(建议大家可以看看《the little schemer》,看完肯定能更好的掌握递归思想,并且对学习clojure大有好处)现在我们再来个稍微难点的例子,我们会递归迭代一组数字,然后搜集遍历过程中得到的前十个偶数。注意这个例子和前面不同的是,我们每次递归(recur)传入的参数是多个,而不是一个。recur后面参数其实是和loop的第一个向量参数中的绑定参数(data、n、n-count、result)是一一对应的,大家仔细观察一下。
(loop [data (range 1 101) n (first data) n-count 0 result nil] ;; result 初始为空列表 (if (and n (< n-count 10)) ;;递归结束条件 (if (even? n) (recur (rest data) (first data) (inc n-count) (cons n result)) (recur (rest data) (first data) n-count result)) (reverse result))) ;;递归结束后,反转结果列表
我们可以做的更好一点,就是把上面定义成一个递归函数:
=>(defn take-evens ;;我们定义的递归函数 ([x nums](take-evens x nums 0 nil)) ;;参数模式一 ([x nums n-count result] ;;参数模式二 (if (empty? nums) ;;递归结束条件一 (reverse result) (if (< n-count x) ;;递归结束条件二 (let [n (first nums)] (if (even? n) (recur x (rest nums) (inc n-count) (cons n result)) (recur x (rest nums) n-count result))) (reverse result)))))#'user/take-evens
;;取出1到100中前十个偶数=>(take-evens 10 (range 1 101))(2 4 6 8 10 12 14 16 18 20);;取出1到100宗前5个偶数=>(take-evens 5 (range 1 101))(2 4 6 8 10)
Clojure惰性序列的头保持问题 - climbdream的个人空间 - 开源中国社区
https://my.oschina.net/clopopo/blog/150130