初探Clojure

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

    屏幕中就会显示:

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

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

      如你所见,Clojure 所拥有的 REPL 环境可以快速地与你进行交互 —- 表达式 [2] 被立即执行。
    • 第 1 行为我们在 REPL 中输入的内容,比如使用键盘输入。这段代码的含义是,告诉 REPL我们要执行 print 函数,以及提供函数所需的参数
    • 第 2 行的 hello world!print 函数的副作用。(而我们所需要的正是这个副作用)
    • 第 3 行的 nilprint 函数的返回值 [4] ,print 函数始终返回 nil。( nilClojure 中表示空值)

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

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

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

    1. 函数的名称。
    2. 函数的参数。
    3. 函数的返回值。

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

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

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

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

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

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

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

    1. hello world! hello again! bye!
    2. 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. 外层函数发现内层所有的参数都已经求值完毕,

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

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

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

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

    1. I love Rock!!!
    2. nil
    3. 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

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

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

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

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

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

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