初探Clojure

欢迎来到 Clojure 的世界。

让我们先从最经典的 hello world 开始吧。

我们使用键盘在 REPL 的输入框里输入 (print "hello world!"),回车!

屏幕中就会显示:

  1. => (print "hello world!")
  2. hello world!
  3. nil

(如果你还不知道怎么启动 REPL,你可以看一下这篇文章:“最小化”运行 Clojure REPL

首先说明本文的几个约定

  • => 后面跟随的内容,表示在 REPL [1] 里输入的内容
  • 在 => 之后另起新行出现的代码,为 REPL 返回 (即 REPL 中的 Print) 的内容

    如你所见,Clojure 所拥有的 REPL 环境可以快速地与你进行交互 —- 表达式 [2] 被立即执行。

现在让我们看看这三行代码分别表示什么吧

  • 第 1 行为我们在 REPL 中输入的内容,比如使用键盘输入。这段代码的含义是,告诉 REPL我们要执行 print 函数,以及提供函数所需的参数
  • 第 2 行的 hello world!print 函数的副作用 [3] 。(而我们所需要的正是这个副作用)
  • 第 3 行的 nilprint 函数的返回值 [4] ,print 函数始终返回 nil。( nilClojure 中表示空值)

所以我们可以大概知道 REPL 是怎么运行的:

  • 首先,他接受你的输入。
  • 然后,执行你所输入的代码,如果有副作用就会触发副作用。
  • 最后,它返回你所输入的代码的值。REPL 总是把你所输入的表达式的值在最后一行显示出来

函数,是 Clojure 里最为重要也是最为基本的组成部分。

就如同你在中学数学学习到的 f(x,y) 一样,函数一般由三部分组成:

  1. 函数的名称。

    print。在 Clojure 中,它写在在小括号 () 中的第一个位置。

  2. 函数的参数。

    如果函数可以接受多个参数,多个参数之间用空格隔开。(也可以用 ,)

  3. 函数的返回值。

    也就是函数的值。

所以 f(x,y)Clojure里就表示为 (f x y)

此例中的 print 函数
它接收任意数量的参数,
它的返回值永远是 nil,也就是空,空值。

print 函数除了返回值之外,还拥有一个“副作用”,那就是它会依次把每个参数的值显示在屏幕上 。(准确来说是 *out* 输出流)

函数像是一个黑盒子,你往里扔参数,他向你扔出返回值。
假如除此之外,这个黑盒子还打了你一巴掌,那这一巴掌就是函数的“副作用”。
如果你是为了得到你的返回值,那这个函数的“功能”就是返回的这个值。如果你想要享受痛苦,那这一巴掌就是他的“功能”。

这里我们显然利用的是 print 函数的副作用,对我们来说它才有用。
print 函数的返回值永远为 nil,所以也就不那么重要了。

Clojure 试图求值一切
函数的值等于它的返回值,而字符串的值就简单的等于他看起来的样子。
(双引号 “” 中的内容称之为字符串,它可以用来存储简单的文字或者数据,是程序设计语言中非常常见的 “明星” 。)

你可能对上面这一大堆话并不是很理解。没关系,我们多看例子

比如我们可以给 print 函数更多的参数

  1. => (print "hello world!" "hello again!" "bye!")
  2. hello world! hello again! bye!
  3. nil

或者一个参数也不给它

  1. => (print)
  2. nil

观察结果

我们看到 print 函数果然显示了它的副作用 —- 依次显示每个参数的值。

例外地,如果没有参数,它自然也就没有副作用可以被触发。

最后,它的返回值 nil 总是在最后一行被显示。

Clojure 的“括号表示法”是可以嵌套的

  1. => (print (print "I love Rock!!!"))
  2. I love Rock!!!nil
  3. nil

为什么会出现这种结果呢?
重复一遍,Clojure 试图求值一切内容
函数的值是它的返回值,字符串的值是它本身…
这个例子的执行步骤是这样的

  1. 从左往右,找到第一个括号要执行的函数为 print
  2. print 函数的副作用是打印每个参数的值
  3. 但是这个参数的值无法直接确定,因为它并不是一个可以被直接求值的东西 —- 它又是一个函数。而函数也是有值的,函数的值就是它的返回值!
  4. 程序转而执行内层的 (print "I love Rock!!!") 。字符串的值可以直接被得到。所以内层 print 函数发现它所有的参数都可以直接被求值。于是它就开始发挥它的副作用了 —- 把每个参数的值打印出来,I love Rock!!! 就显示出来了。
  5. 此时内层函数的值确认了 —- 内层 print 函数的值等于它的返回值 nil (虽然你一眼就能知道返回值永远为 nil,但计算机程序没有这个本事,它只能执行之后才能知道)
  6. 外层函数发现内层所有的参数都已经求值完毕,

    (如果这个时候时间静止的话,由于内层的“谜题”已经被解开,那我们的代码可能就会变成像这个样子)

    1. (print nil)
  7. 此时外层 print 函数的副作用发生!输出每个参数的值,即输出内层函数的值 —- nil
  8. 最后外层函数返回值 nil 显示在屏幕上。

如果你使用一些集成开发环境,那么你可以看到 print 函数的副作用所显示的 nilprint 函数的返回值 nil 的显示效果(如颜色和字体)看起来是不同的

一整句嵌套的表达式的返回值只有一个!它取决于最外层的那个函数的返回值!此例中即为最外层的那个print 的值 nil

同样,你可能对上面这一大堆话并不是很理解
我们再来几个例子
这次来介绍一个新的函数 println
它与print 函数的唯一不同在于,每次产生副作用打印时,自动在末尾换行

  1. => (println (println "I love Rock!!!"))
  2. I love Rock!!!
  3. nil
  4. nil

复杂的例子

  1. => (println (print "I love Rock!!!") (println "I love Rock too!!!") (print "I love you..."))
  2. I love Rock!!!I love Rock too!!!
  3. I love you...nil nil nil
  4. nil

可以看到,最外层 println 函数在等待所有参数的值依次求值完毕后,副作用发生,一次性输出了三个 nil ,然后显示了自己的返回值
函数返回值是自动换行显示的(有些 REPL 环境并不自动换行,取决于具体实现),println 函数的换行效果指的是在副作用的末尾换行,即打印完毕后换行,此例中是在 “I love Rock too!!!” 后换了一行

作为一个程序设计语言,计算自然是最基础的。
但与其它语言或者日常习惯不同的一点,Clojure 的计算表示使用前缀表达式
即运算符号同样是个普通的函数(甚至不是一个关键字)
而函数理所当然要放在括号的第一个位置

  1. => (+ 1 1)
  2. 2

加法函数 + 接收任意数量的可运算的表达式作为参数,它的返回值是各个参数的和,它没有副作用。
同样是可以嵌套的

  1. => (+ 3 (+ 1 22))
  2. 26

等价于

  1. => (+ 3 1 22)
  2. 26

注意 3 和 (+ 1 22) 之间有个空格,因为这两个表达式为外层函数的两个参数,自然要用空格隔开(数字 3 也是一个正确的表达式)
与之前的例子相似,在遇到有参数需要进一步求值时,会先求内层的值
这种做法使得你无需记忆无趣又无用的运算优先级
因为每个运算符号一定在括号的第一个位置,所以你总是能一层一层的找到唯一的计算顺序

  1. => (+ 2 (* 8 2));等价于中缀表达式 2 + 8 * 2
  2. 18
  1. => (* 2 (+ 8 2));等价于中缀表达式 2 * (8 + 2)
  2. 20

现在你已经初步了解了 Clojure 的执行过程与它的语法
接下来你会逐渐适应这种看似奇怪的表达方式
最终陶醉于这种表达方式所带来的优雅、简洁和便利
以及这种强大的语言所产生的无法抗拒的魅力

[1]: REPL 即 Read-Eval-Print Loop —- “读取-求值-输出” 循环

[2]: 表达式:你可以简单理解为一个可以被 Clojure 所执行的代码

[3]: 副作用(Side effect):副作用是指,表达式被求值后,对外部世界的状态做的某些改变。当我们对一个如 (+ 1 2) 这样纯粹的 Lisp 表达式求值时,没有产生副作用。它只返回一个值。但当我们调用 print 时,它不仅返回值,还印出了某些东西。这就是一种副作用。(引用自ANSI Common Lisp 中文翻譯版

[4]: 返回值即为表达式执行后的值,同时是表达式本身的值,Clojure 中所有的表达式都有值