- 初探Clojure
- 让我们先从最经典的 hello world 开始吧。
- 首先说明本文的几个约定
- 现在让我们看看这三行代码分别表示什么吧
- 函数,是
Clojure里最为重要也是最为基本的组成部分。
初探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!!!nilnil
为什么会出现这种结果呢?
重复一遍,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!!!nilnil
复杂的例子
=> (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 nilnil
可以看到,最外层 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 * 218
=> (* 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 中所有的表达式都有值 ↩
