`
huangz
  • 浏览: 322297 次
  • 性别: Icon_minigender_1
  • 来自: 广东-清远
社区版块
存档分类
最新评论

Clojure 快速入门指南:1/3

阅读更多

导读

 

本文的目标是为熟悉 Ruby、Python或者其他类似语言、并对 Lisp 或者函数式编程有一定程度了解的程序员写的 Clojure 快速入门指南。

 

为了让文章尽可能地精炼且简单易懂,本文有以下三个特点:

 

一:不对读者的知识水平作任何假设,当遇上重要的知识点时,只给出 wikipedia 等网站的链接引用而不对知识点进行解释,有需要的读者可以沿着链接查看,没需要的直接略过就行了。

 

二:和第一条类似,没有介绍所有 Clojure 的语法和库,但会给出详细资料的引用链接。

 

三:将 Clojure 中的各项语法和其他常用语言,比如 Ruby 、 Python 和 JAVA 作类比,这样可以帮助有经验的读者快速了解 Clojure 的各项功能(尽管它们在实现细节和真正概念上可能有区别)。

 

阅读完本文后,你应该可以对 Clojure 有所了解,并熟悉一些用 Clojure 写程序的惯用法。

 

 

安装并运行 Clojure

 

Clojure 运行在 JRE (JAVA Runtime Environment) 之上,因此,你需要先安装 JRE ,然后到 Clojure 的主页下载最新版的 Clojure 。

 

安装 JRE 和 Clojure 的方法因使用的系统而不同,如果你和我一样使用 Archlinux ,那么执行命令 sudo pacman -S jre clojure 即可,其他系统可以按照 JAVA 主页 和 Clojure 主页上的方法来操作:

 

安装 JRE: http://www.oracle.com/technetwork/java/javase/downloads/index.html

 

安装 Clojure :http://clojure.org/getting_started


如果一切正常,那么现在你应该可以使用命令 clj 来调出 Clojure 的 REPL 程序了(在你的电脑上使用的命令可能有不同),用 Clojure 跟大家说声好吧:

 

$ clj 
Clojure 1.3.0
user=> (str "Hello world!")
"Hello world!"

 

 

定义变量

 

Clojure 中的变量通过 def 来定义:

 

user=> (def greet "Good Morning")
#'user/greet

user=> greet
"Good Morning"

 

在上面的代码中我们定义了一个 greet 变量,将它和字符串 "Good Morning" 绑定,类似的 Python 代码是:

 

greet = "Good Morning"

 

一个变量可以重复地绑定:

 

user=> (def lucky-number 10086)
#'user/lucky-number

user=> lucky-number
10086

user=> (def lucky-number 123123)
#'user/lucky-number

user=> lucky-number
123123
  

上面的代码用同一个变量进行了两次绑定,注意,虽然在同一段程序里反复使用一个同名变量在 Ruby 或者 Python 之类的语言中非常常见(赋值),但这种用法在 Clojure 中并不是一个好习惯(原因我迟些会告诉你),上面的代码只是告诉你可以这么做,并不是推荐你写这样的代码。

 

 

分隔符

 

你可能已经注意到了,在上面的 lucky-number 例子中,我们使用中划线 "-" 作为字母的分隔符,而不是 Ruby 和 Python 中常用的下划线 “_" ,的确如此,这是一个 Clojure 的惯用法。

 

下面是一些 Python 的变量名:

 

selected_elements

get_record_by_id

show_me_your_money
  

它们在 Clojure 中的写法是:

 

selected-elements

get-record-by-id

show-me-your-money

 

 

定义函数

 

定义函数的方式和定义变量很相似,不过定义函数使用的是 defn ,而不是 def。 

 

比如现在我们要定义一个更先进的问候语系统,它可以根据你输入的问候语而做出不同的反应 —— 如果你输入 “Good Morning!”,它就返回 "Morning!" ,对于其他情况,它返回  “Hello!" :

 

user=> (defn greet-replay [you-say]
           (if (= you-say "Good Morning!")
               "Morning!"
               "Hello!"))
#'user/greet-replay

user=> (greet-replay "Hi!")
"Hello!"

user=> (greet-replay "Hello, huangz!")
"Hello!"

user=> (greet-replay "Good Morning!")
"Morning!"
  

让我们一行行分析 greet-replay 函数:

 

首先,第一行,我们用 defn 定义了一个叫 greet-replay 的函数,它接受一个参数 you-say ,其中,参数被方括号所包围。

 

然后在第二行,greet-replay 函数使用了 if 形式(form),它和其他很多语言的 if 一样,都是接受一个布尔值,然后根据布尔值的真假来决定执行哪一个分支。

 

在这里,我们使用了代码 (= you-say "Good Morning!") 对比输入的参数和 "Good Morning!" 是否相等,如果相等,那么返回 "Morning!" ,否则的话,返回 "Hello!" 。

 

这里给出一个 Python 写的 greet-replay 函数作为参考:

 

>>> def greet_replay(you_say):
...   if you_say == "Good Morning!":
...     return "Morning!"
...   else:
...     return "Hello!"
... 

>>> greet_replay("Hi!")
'Hello!'

>>> greet_replay("Good Morning!")
'Morning!'
  

可以看出, 两个版本除了在分隔符方面的差别之外,还有两点比较明显的不同:

 

  1. Clojure 的 if 没有 else , Clojure 中 if 的两个分支只用空白或空行隔开即可。
  2. Clojure 将函数执行的最后一个表达式的值作为函数的返回值,因此我们不必像 Python 那样显式地使用 return 。

在第二点方面, Ruby 和 Clojure 是一样的:

 

irb(main):005:0> def greet_replay(you_say)
irb(main):006:1>   if you_say == "Good Morning!"
irb(main):007:2>     "Morning!"
irb(main):008:2>   else
irb(main):009:2*     "Hello!"
irb(main):010:2>   end
irb(main):011:1> end
=> nil
irb(main):012:0> greet_replay("Hi")
=> "Hello!"
irb(main):013:0> greet_replay("Good Morning!")
=> "Morning!"

 

 

前序操作符

 

你可能已经注意到,在上面的 greet_replay 函数中,我们对比两个字符串的方式和 Ruby 和 Python 有些不同,我们将 = 号放在前面:

 

(= you-say "Good Morning!")

 

它和 Python 或者 Ruby 对比的方法都不同:

 

if you_say == "Good Morning!"

 

我们称 Clojure 所使用的方式称之为前序操作符,而 Python 和 Ruby 所使用的方式称为中序操作符。

 

在 Clojure 中,我们总使用前序操作符 —— 因为 Clojure 没有操作符,只有函数、特殊形式(special form)和宏,当一个函数/特殊形式/宏被使用的时候,它总是被放在表达式的第一个位置上,用作前序操作符。

 

比如在上面的例子中,Clojure 的 = 函数完成的就是 Python 的 == 操作符的工作:对一个字符串进行对比。

 

 

谓词函数

 

在之前的 greet-replay 函数里,我们使用了 = 函数来测试两个字符串是否相等,继而决定 if 的最终走向。


这种测试并返回 true 或者 false 的对比,我们一般称之为谓词,或者分支判断,在 Clojure 中,谓词一般在最后加一个问号 "?" 作为标识,这也是一个 Clojure 惯用法。

 

比如说,我们可以将这个测试抽象成一个新的函数 same-greeting? 

 

user=> (defn same-greeting? [you-say i-want]
           (= you-say i-want))
#'user/same-greeting?

user=> (same-greeting? "Hi!" "Morning!")
false

user=> (same-greeting? "Hi!" "Hi!")
true
  

然后可以使用新的 same-greeting? 重写之前的 greet-replay 函数:

 

user=> (defn greet-replay [you-say]
           (if (same-greeting? you-say "Good Morning!")
               "Morning!"
               "Hello!"))
#'user/greet-replay

user=> (greet-replay "Hi!")
"Hello!"

user=> (greet-replay "Good Morning!")
"Morning!"
  

谓词函数增强了代码的可读性,现在的 greet-replay 函数读起来就像一句普通的英语一样,因为这个原因,在 Clojure 的标准库大量使用了谓词函数,比如 false? 、 nil? 、 sorted? 、 zero? ,等等。

 

使用 Ruby 的读者应该对带问号的谓词函数非常熟悉,因为 Clojure 和 Ruby 的问号惯用法都同样遗传自 Lisp 。

 

 

阶乘函数

 

在前面的介绍中,我们用函数写了一个简单的 greet-replay ,这一次,让我们用函数做一点更复杂的事情:计算阶乘。

 

阶乘是一个数学定义,它可以用符号 N! 表示,代表这样一个概念:计算从 1 开始,到某个数 N 的所有数的乘积。

 

比如说, 5! = 1 * 2 * 3 * 4 * 5 = 120 ,而 10! = 1 * 2 * 3 * 4 * 5 * 6 * 7 * 8 * 9 * 10 = 3628800,等等。

 

将这个概念进一步泛化,我们可以写出一个函数 factorial (阶乘) ,它接受一个参数 n ,并计算出这样一个乘法序列: 1 * 2 * 3 * ... * (n-1) * n 。

 

根据公式,可以很快地给出一个 Python 版本的 factorial 函数:

 

>>> def factorial(n):
...     result = 1
...     i = 1
...     while i <= n:
...         result *= i
...         i += 1
...     return result
... 

>>> factorial(5)
120

>>> factorial(10)
3628800
  

上面的 factorial 定义了两个变量 i 和 result ,然后使用 while 迭代计算出阶乘。

 

很明显,如果我们想要在 Clojure 中计算阶乘函数,那么一个类似 Python 中的 while 关键字那样可以进行迭代的功能就是必不可少的 —— Clojure 中的确有类似的东西,它就是 loop 形式

 

以下是一个使用 loop 形式写的阶乘函数:

 

user=> (defn factorial [n]
           (loop [i 1, result 1]
               (if (> i n)
                   result
                   (recur (inc i) (* i result)))))
#'user/factorial

user=> (factorial 5)
120

user=> (factorial 10)
3628800

 

嗯,这个 factorial 函数有点复杂,需要花些时间解释一下:

 

第一行是我们的老朋友 defn ,它定义一个名为 factorial 的函数,factorial 函数只接受一个参数 n 。

 

第二行是我们的新朋友,loop ,它和 defn 的使用方式有点类似,同样都是使用一个大括号将一些东西包围起来,这里是 [ i 1, result 1] ,这是什么意思呢?嗯,这就是说,要在 loop 形式之内,构建两个新的临时变量 i 和 result ,它们两个的值都是 1 。这些临时变量只能用在 loop 包围的地方。

 

第三行是我们的另一个老朋友 if ,它判断如果变量 i 比 参数 n 还要大的时候,就返回变量 result 作为函数的值。

 

第五行是 factorial 函数的关键,整个语句是 (recur (inc i) (* i result)) ,其中, recur 形式在 loop 形式当中被使用的时候,它会跳到 loop 所在的地方,并用 recur 参数里的值去更新 loop 形式里面的值

 

比如说,当我们执行 (factorial 5) 的时候,factorial 内部的执行序列是这样的:

 

  1. 定义 i = 1, result = 1 ,因为 (> i n) 测试失败,所以 (recur (inc i) (* i result)) 被执行,并更新 loop 变量的值。
  2. 因为 recur 的作用,loop 的两个变量被更新,现在 i 和 result 分别的值为 i = 2, result = 2, 测试 (> i n) 再次失败,执行 recur 。
  3. 因为 recur 的作用,现在 i = 3, result = 6 ,测试 (> i n) 失败,执行 recur 。
  4. 变量更新,现在 i = 4, result = 24 , 测试 (> i n) 失败,recur 执行。
  5. 变量更新,现在 i = 5, result = 120 ,测试 (> i n) 失败, recur 执行。
  6. 变量更新,现在 i = 6, 测试 (> i n) 成功, result 被返回。
  7. 结果,(factorial 5) 的值为 120

 

递归

 

"坑爹“,你可能会这样想,”huangz 这只菜鸟完全不会写 Python 代码, factorial 函数应该这样写才对:“

 

>>> def factorial(n):
...     result = 1
...     for i in range(1, n+1):
...         result *= i
...     return result
... 

>>> factorial(5)
120

>>> factorial(10)
3628800
 

你是对的, factorial 这么写更简洁一些(事实上,这个写法在 n 很大的时候会出现性能问题),但是,那样的话,对比一看,我们忽然发现一个严重的问题: 解决同一个问题, Clojure 使用的代码居然比 Python 要复杂!

 

这怎么可能!?牛人们都说 Lisp 是世界上最强大的语言,那为什么解决这么一个简单的阶乘问题, Clojure 居然干不过 Python ,是什么地方出了问题呢?

 

嗯,实际上,在上面的定义阶乘函数的问题上,造成 Clojure 的解法比 Python 更复杂的原因,是因为我们没有使用 Clojure 去思考。

 

Clojure 是一门函数式语言,它和常用的语言比如 Python 或者 Ruby 有一些类似的地方,但是本质上 Clojure 和 Ruby 或者 Python 都非常不同 —— 比如说,在 Python 和 Ruby 中, 我们经常使用 for 关键字和 each 方法对一个对象(列表,集合,数组,等等)进行遍历,这种遍历是以迭代的方式进行的,但是,在 Clojure 中,人们更愿意使用递归而不是迭代

 

什么是递归?简单说来,就是一个函数可以通过调用它自身来解决问题。

 

举个例子, 以下是 Clojure 递归版的阶乘函数,用它和之前的 loop 版本或者 Python 的 for 版本比较,应该能帮助你理解递归是怎么一回事:

 

user=> (defn factorial [n]
           (if (= n 1)
               1
               (* n (factorial (dec n)))))
#'user/factorial

user=> (factorial 5)
120

user=> (factorial 10)
3628800
  

上面的 factorial 比之前写过的两个版本都更简洁、更容易让人理解。

 

并且,要注意到,它和迭代版本使用的是不同的公式:

 

factorial(n) = 1 if  n == 1

factorial(n) = n * (factorial n-1) if n > 1

 

这个公式和之前的 n! = 1 * 2 * 3 * ... * (n-1) * n 计算出的结果完全相同(本质上是一样的),但新的公式是递归地定义的,旧公式则不是 —— 新旧公式的区别,大概就是递归思考和迭代思考的区别,根据两种不同的思考方式,我们写出了完全不同的函数。

 

记住,要成为 Clojure 高手,你必须先加入递归俱乐部!

 

递归俱乐部有两条入门规则:

 

  1. 使用递归思考,写递归函数解决递归问题。
  2. 不使用像 loop 那样具有迭代思想的技术,并将它们视为优美代码的大敌。
牢记并在你的代码中实践这两条规则,你很快就能晋升成为递归俱乐部的正式会员,继而走上成为 Clojure 高手的光辉大道。。。

更上一层楼

我们定义的递归版本 factorial 函数虽然简单,但是实际上,还有一种更简单的解法 —— 使用 reducerange 函数,我们可以将 factorial 的代码数减至两行:

user=> (defn factorial [n]
           (reduce * (range 1 (inc n))))
#'user/factorial

user=> (factorial 5)
120

user=> (factorial 10)
3628800
 
新版 factorial 非常酷,让我们看看它是怎么来的。

首先, range 函数负责生成一个从 1 到 n 的数值列表,类似于 Python 的 range 函数:

user=> (range 1 (inc 10))
(1 2 3 4 5 6 7 8 9 10)
 
然后,我们使用 reduce 和乘法 * ,先将列表整个展开成一个乘法计算序列,然后再进行收缩计算。

比如说,执行代码 (reduce * (range 1 (inc 5))) ,代码运行时的状态大概如下:

(reduce * (1 2 3 4 5))
(* 1 (reduce * (2 3 4 5)))
(* 1 (* 2 (reduce * (3 4 5))))
(* 1 (* 2 (* 3 (reduce * (4 5)))))
(* 1 (* 2 (* 3 (* 4 (reduce * (5)))))) 
(* 1 (* 2 (* 3 (* 4 5))))
(* 1 (* 2 (* 3 20)))
(* 1 (* 2 60))
(* 1 120)
120
 
最新版本的 factorial 函数使用的是和之前一样的公式(从展开的计算链条里应该能看出这一点),但新版的 factorial 函数处在抽象层次的一个非常高的位置上,对比之前的递归版本以及 loop 版本,它考虑的细节最少,写出的代码也最少,因为它使用 Clojure 来思考,并使用了其中几个相当强大的技术:递归、 高阶函数reduce(也叫fold)列表

一般来说,如果你以正确的方式来写 Clojure 代码,最终得出的代码会非常少且紧凑 —— 这也是为什么人们喜欢使用 Clojure (以及其他函数式语言)编程的原因。

小结

这一章主要介绍了 Clojure 的变量和函数的定义,以及递归的使用,并在最后的例子中对高阶函数等强有力的函数式编程技术作了一个快速而简单的了解。

待续。。。

嗯, 关于 Clojure 还有很多很酷的地方需要讲讲,比如它的数据结构、宏、数据类型、怎样利用 JAVA 平台的优势、并发、包,等等,我会在以后的章节继续给各位介绍这些好玩的东西。









分享到:
评论
9 楼 lot1 2014-08-01  
part 2? part 3?
8 楼 superlittlefish 2014-07-18  
希望楼主完成剩余工作, 写的太好了.
7 楼 Reset 2014-01-08  
赞  可惜只有1篇...
6 楼 dingbuoyi 2013-04-28  
怎么只写了一篇。。。不是应该有3篇吗
5 楼 dingbuoyi 2013-04-28  
楼主NB!
4 楼 csophys 2012-07-13  
写得非常好。收获好多,打算学Clojure了。期待后续!
3 楼 nwf5d 2012-06-27  
写的很好,
期待后续。。。。
2 楼 huangz 2012-01-14  
linkerlin 写道
写的很好。
非常好懂。
保持这种风格继续写下去吧。


是的,我想用『对比』的方法会让有经验的人比较快看明白,后续篇正在构思中。。。

:)
1 楼 linkerlin 2012-01-05  
写的很好。
非常好懂。
保持这种风格继续写下去吧。

相关推荐

    cqrs-server:使用 Onyx、Datomic、DynamoDB、Kafka 和 Zookeeper 的固执的 Clojure CQRSES 实现

    快速入门指南: 本地安装dynamodb 从以下位置获取 dynamodb: ://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Tools.DynamoDBLocal.html 然后运行: java -Djava.library.path=./DynamoDBLocal_...

    Clojure Data Analysis Cookbook

    1. **基础篇**:这一部分将引导读者入门 Clojure 编程语言的基本概念和语法,为后续深入学习打下坚实的基础。 - **Clojure 入门**:介绍 Clojure 的安装过程、REPL(Read-Eval-Print Loop)环境的使用方法以及简单...

    Clojure资格web资源

    "Quick Ref for Clojure Core.htm"是一个Clojure核心库的快速参考指南,它为开发者提供了Clojure核心库中各种函数和宏的速查表。这种文档通常包含函数签名、简短描述以及示例,帮助开发者迅速找到他们需要的函数或...

    clojure-shopshop:一日研讨会的基本Clojure培训材料

    【标题】:“clojure-shopshop”是一场关于Clojure的基础培训研讨会,主要目的是为参与者提供一个快速入门Clojure编程语言的平台。这个研讨会通过一系列的实践活动和讲解,帮助初学者理解Clojure的核心概念和语法。 ...

    clojure网站:clojure.org网站

    - **教程**:针对初学者,网站提供了入门教程,帮助快速理解 Clojure 的基本概念和语法。 - **新闻与更新**:网站会发布 Clojure 的最新版本信息,以及相关的工具和框架更新。 - **社区资源**:包括论坛、邮件...

    Pragmatic - Web.Development.with.Clojure.Jan.2014.pdf

    - **Sam Griffith Jr.**(Interactive Web Systems, LLC的多语言程序员)表示本书是一本快速入门指南,帮助读者快速上手并构建真正的Web应用。 #### 五、结论 - 《Web Development with Clojure》是一本全面介绍...

    【Clojure入门之windows环境安装】Leiningen快速手动安装指南-附件资源

    【Clojure入门之windows环境安装】Leiningen快速手动安装指南-附件资源

    werkbank:Clojure的块状可视化编程

    werkbank 是一个专门为 Clojure 语言设计的块状可视化编程工具,它借鉴了 Google 的 Blockly 概念,为程序员提供了一种通过拖放图形化块来编写代码的方式,尤其适合教育和初学者入门。werkbank 的核心目标是降低 ...

    clojurice:Clojure中用于全栈Web应用程序的自觉入门应用程序

    【clojurice】是专为Clojure语言设计的一个全栈Web应用程序模板,旨在帮助开发者快速启动他们的项目。这个模板尤其适合新手,因为它提供了一个自解释的起点,让初学者能够理解Clojure和ClojureScript在构建Web应用中...

    绿灯:Clojure集成测试框架

    此外,文档通常会包含安装指南、快速入门教程和详细的API参考,以便于快速上手。 总之,绿灯(Greenlight)是Clojure开发者进行集成测试的一个强大工具,它的设计思路和特性使得在编写和维护测试时能保持代码的清晰...

    令人敬畏的clojure

    3. 学习资源:包含教程、文档、书籍和在线课程,帮助开发者快速入门Clojure,如"Programming Clojure"书籍、"4Clojure"在线练习平台等。 4. 社区和论坛:Clojure有活跃的社区,如Clojurians Slack频道、Clojure ...

    Seven Web Frameworks in Seven Weeks

    - **Web开发初学者**:对于刚接触Web开发的新手来说,本书可以作为一个快速入门指南,帮助他们了解当前流行的Web框架及其背后的原理。 - **技术选型决策者**:对于正在寻找合适的Web开发工具的技术团队领导者或项目...

    edmondson:一个可扩展的,易于使用的工具包,用于分析和评分调查结构,例如心理安全和生成文化。 支持多种调查系统,例如Google Forms和Qualtrics

    快速开始本节是入门的快速指南。 请参阅以获取更多文档。先决条件您不需要了解Java或Clojure即可使用它。 这些仅仅是运行时相关性,需要安装才能运行。1. Java / JDK 您必须已安装Java / JDK(例如, )。 注意:...

    opencv_tutorials

    - 查找表(LUT)用于快速查找图像的值。 - 实现图像增强等效果。 - **时间测量:** - 使用`cv::getTickCount`和`cv::getTickFrequency`函数来测量代码执行的时间。 **2.3 矩阵掩模操作** - **掩模概念:** - ...

Global site tag (gtag.js) - Google Analytics