初探Clojure
欢迎来到 Clojure 的世界。
让我们先从最经典的 hello world 开始吧。
我们使用键盘在 REPL 的输入框里输入 (print "hello world!")
,回车!
屏幕中就会显示:
=> (print "hello world!")
hello world!
nil
(如果你还不知道怎么启动 REPL
,你可以看一下这篇文章:“最小化”运行 Clojure REPL )
首先说明本文的几个约定
- => 后面跟随的内容,表示在
REPL
[1] 里输入的内容 - 在 => 之后另起新行出现的代码,为
REPL
返回 (即REPL
中的Print
) 的内容
如你所见,Clojure
所拥有的REPL
环境可以快速地与你进行交互 —- 表达式 [2] 被立即执行。
现在让我们看看这三行代码分别表示什么吧
- 第 1 行为我们在 REPL 中输入的内容,比如使用键盘输入。这段代码的含义是,告诉
REPL
我们要执行print
函数,以及提供函数所需的参数 - 第 2 行的
hello world!
为print
函数的副作用 [3] 。(而我们所需要的正是这个副作用) - 第 3 行的
nil
为print
函数的返回值 [4] ,print 函数始终返回 nil。(nil
在Clojure
中表示空值)
所以我们可以大概知道 REPL
是怎么运行的:
- 首先,他接受你的输入。
- 然后,执行你所输入的代码,如果有副作用就会触发副作用。
- 最后,它返回你所输入的代码的值。REPL 总是把你所输入的表达式的值在最后一行显示出来
函数,是 Clojure
里最为重要也是最为基本的组成部分。
就如同你在中学数学学习到的 f(x,y)
一样,函数一般由三部分组成:
- 函数的名称。
如
print
。在Clojure
中,它写在在小括号()
中的第一个位置。 - 函数的参数。
如果函数可以接受多个参数,多个参数之间用空格隔开。(也可以用 ,)
- 函数的返回值。
也就是函数的值。
所以 f(x,y)
在 Clojure
里就表示为 (f x y)
此例中的 print
函数
它接收任意数量的参数,
它的返回值永远是 nil
,也就是空,空值。
而 print
函数除了返回值之外,还拥有一个“副作用”,那就是它会依次把每个参数的值显示在屏幕上 。(准确来说是 *out*
输出流)
函数像是一个黑盒子,你往里扔参数,他向你扔出返回值。
假如除此之外,这个黑盒子还打了你一巴掌,那这一巴掌就是函数的“副作用”。
如果你是为了得到你的返回值,那这个函数的“功能”就是返回的这个值。如果你想要享受痛苦,那这一巴掌就是他的“功能”。
这里我们显然利用的是 print
函数的副作用,对我们来说它才有用。
而 print
函数的返回值永远为 nil
,所以也就不那么重要了。
Clojure
试图求值一切
函数的值等于它的返回值,而字符串的值就简单的等于他看起来的样子。
(双引号 “” 中的内容称之为字符串,它可以用来存储简单的文字或者数据,是程序设计语言中非常常见的 “明星” 。)
你可能对上面这一大堆话并不是很理解。没关系,我们多看例子
比如我们可以给 print
函数更多的参数
=> (print "hello world!" "hello again!" "bye!")
hello world! hello again! bye!
nil
或者一个参数也不给它
=> (print)
nil
观察结果
我们看到 print
函数果然显示了它的副作用 —- 依次显示每个参数的值。
例外地,如果没有参数,它自然也就没有副作用可以被触发。
最后,它的返回值 nil
总是在最后一行被显示。
Clojure
的“括号表示法”是可以嵌套的
=> (print (print "I love Rock!!!"))
I love Rock!!!nil
nil
为什么会出现这种结果呢?
重复一遍,Clojure
试图求值一切内容
函数的值是它的返回值,字符串的值是它本身…
这个例子的执行步骤是这样的
- 从左往右,找到第一个括号要执行的函数为
print
print
函数的副作用是打印每个参数的值- 但是这个参数的值无法直接确定,因为它并不是一个可以被直接求值的东西 —- 它又是一个函数。而函数也是有值的,函数的值就是它的返回值!
- 程序转而执行内层的
(print "I love Rock!!!")
。字符串的值可以直接被得到。所以内层print
函数发现它所有的参数都可以直接被求值。于是它就开始发挥它的副作用了 —- 把每个参数的值打印出来,I love Rock!!! 就显示出来了。 - 此时内层函数的值确认了 —- 内层
print
函数的值等于它的返回值nil
(虽然你一眼就能知道返回值永远为nil
,但计算机程序没有这个本事,它只能执行之后才能知道) 外层函数发现内层所有的参数都已经求值完毕,
(如果这个时候时间静止的话,由于内层的“谜题”已经被解开,那我们的代码可能就会变成像这个样子)
(print nil)
- 此时外层
print
函数的副作用发生!输出每个参数的值,即输出内层函数的值 —-nil
。 - 最后外层函数返回值
nil
显示在屏幕上。
如果你使用一些集成开发环境,那么你可以看到 print
函数的副作用所显示的 nil
和print
函数的返回值 nil
的显示效果(如颜色和字体)看起来是不同的
一整句嵌套的表达式的返回值只有一个!它取决于最外层的那个函数的返回值!此例中即为最外层的那个print
的值 nil
同样,你可能对上面这一大堆话并不是很理解
我们再来几个例子
这次来介绍一个新的函数 println
它与print
函数的唯一不同在于,每次产生副作用打印时,自动在末尾换行
=> (println (println "I love Rock!!!"))
I love Rock!!!
nil
nil
复杂的例子
=> (println (print "I love Rock!!!") (println "I love Rock too!!!") (print "I love you..."))
I love Rock!!!I love Rock too!!!
I love you...nil nil nil
nil
可以看到,最外层 println
函数在等待所有参数的值依次求值完毕后,副作用发生,一次性输出了三个 nil
,然后显示了自己的返回值
函数返回值是自动换行显示的(有些 REPL
环境并不自动换行,取决于具体实现),println
函数的换行效果指的是在副作用的末尾换行,即打印完毕后换行,此例中是在 “I love Rock too!!!” 后换了一行
作为一个程序设计语言,计算自然是最基础的。
但与其它语言或者日常习惯不同的一点,Clojure
的计算表示使用前缀表达式
即运算符号同样是个普通的函数(甚至不是一个关键字)
而函数理所当然要放在括号的第一个位置
=> (+ 1 1)
2
加法函数 + 接收任意数量的可运算的表达式作为参数,它的返回值是各个参数的和,它没有副作用。
同样是可以嵌套的
=> (+ 3 (+ 1 22))
26
等价于
=> (+ 3 1 22)
26
注意 3 和 (+ 1 22)
之间有个空格,因为这两个表达式为外层函数的两个参数,自然要用空格隔开(数字 3 也是一个正确的表达式)
与之前的例子相似,在遇到有参数需要进一步求值时,会先求内层的值
这种做法使得你无需记忆无趣又无用的运算优先级
因为每个运算符号一定在括号的第一个位置,所以你总是能一层一层的找到唯一的计算顺序
=> (+ 2 (* 8 2));等价于中缀表达式 2 + 8 * 2
18
=> (* 2 (+ 8 2));等价于中缀表达式 2 * (8 + 2)
20
现在你已经初步了解了 Clojure
的执行过程与它的语法
接下来你会逐渐适应这种看似奇怪的表达方式
最终陶醉于这种表达方式所带来的优雅、简洁和便利
以及这种强大的语言所产生的无法抗拒的魅力
[1]: REPL 即 Read-Eval-Print Loop —- “读取-求值-输出” 循环 ↩
[2]: 表达式:你可以简单理解为一个可以被 Clojure 所执行的代码 ↩
[3]: 副作用(Side effect):副作用是指,表达式被求值后,对外部世界的状态做的某些改变。当我们对一个如 (+ 1 2)
这样纯粹的 Lisp 表达式求值时,没有产生副作用。它只返回一个值。但当我们调用 print 时,它不仅返回值,还印出了某些东西。这就是一种副作用。(引用自ANSI Common Lisp 中文翻譯版) ↩
[4]: 返回值即为表达式执行后的值,同时是表达式本身的值,Clojure 中所有的表达式都有值 ↩