• 以不变应万变
    • 复习
    • Clojure 里,值是不可变的。
    • 这里再介绍几个对集合进行操作的函数
      • 使用 cons 函数把一些元素添加到集合的头部
      • 使用 conj 函数来添加一些元素到集合中
        • 对于 vector 来说是末尾,但是对 list 来说是头部:
      • 使用 into 函数来把两个集合进行合并。

    以不变应万变

    一个抽象的过程就是寻找变化中的不变量

    在之前的学习中我们学习了如何定义我们的集合,一个很自然的想法就是修改这个集合。

    复习

    我们可以使用 assoc 函数来替换一个集合中的元素

    1. => (assoc ["第一个元素" "第二个元素"] 0 "first")
    2. ["first" "第二个元素"]

    assoc 函数的第一个参数是你要进行替换的集合,第二个参数是你要替换的元素所在“位置”,第三个参数是替换后的元素。

    (注意,索引位置以 0 开始。)

    返回值是修改过后的集合的值。

    然而,Clojure 里集合类型是不可变的。assoc 函数的替换其实并没有改变原集合元素的内容,它返回的是一个“新的”集合。

    (你不用担心浪费存储空间,这个“新”的集合大部分重用了“老”集合。)

    我们可以通过以下代码观察:

    1. => (def my-vec ["第一个元素" "第二个元素"])
    2. #'my-clojure-study.core/my-vec
    3. => (assoc my-vec 0 "first")
    4. ["first" "第二个元素"]
    5. => (print my-vec)
    6. [第一个元素 第二个元素]
    7. nil

    在这段代码中,我们首先定义了一个 vector,然后我们使用 assoc 函数来对这个 vector 进行“替换”。把 0 号元素替换为 “first"。看起来我们已经改动了这个 my-vec。但是当我们使用 print 函数来观察这个集合的时候却发现,他的值没有发生变化。

    那我们如何保存改变之后的值呢?

    我们可以再给这个改变之后的值取一个名字。

    1. => (def my-vec ["第一个元素" "第二个元素"])
    2. #'my-clojure-study.core/my-vec
    3. => (def modifyed-vec (assoc my-vec 0 "first"))
    4. #'my-clojure-study.core/modified-vec
    5. => (print my-vec)
    6. [第一个元素 第二个元素]
    7. nil
    8. => (print modifyed-vec)
    9. [first 第二个元素]
    10. nil

    更常见的做法是把这个改变作为一个值,传递给下一个需要这个值的表达式(如一个函数。我们在之后的定义函数一节里再进行详细介绍)

    Clojure 里,值是不可变的。

    我们为什么需要这种不可变的设计呢?

    一个很重要的原因就是,使用不可变的值可以更为简便的实现多线程,当你持有一个值的引用的时候,你不用担心会有其它线程修改你的值。

    (另一个“文绉绉”的说法是,这样更能实现一个“纯函数”,即不修改其它值或者其他状态的函数 [1] 。)

    这里再介绍几个对集合进行操作的函数

    不再一一叙述每个参数是什么,大家在代码中观察,以及在自己的机器上运行观察结果,然后修改部分参数观察是否符合预期。

    使用 cons 函数把一些元素添加到集合的头部

    1. => (def my-vec ["第一个元素" "第二个元素"])
    2. #'my-clojure-study.core/my-vec
    3. => (cons "第零个元素" my-vec)
    4. ("第零个元素" "第一个元素" "第二个元素")

    使用 conj 函数来添加一些元素到集合中

    1. => (def my-vec ["第一个元素" "第二个元素"])
    2. #'my-clojure-study.core/my-vec
    3. => (conj my-vec "第三个元素")
    4. ["第一个元素" "第二个元素" "第三个元素"]
    5. => (conj my-vec "第三个元素" "第四个元素")
    6. ["第一个元素" "第二个元素" "第三个元素" "第四个元素"]
    7.  
    8. => my-vec
    9. ["第一个元素" "第二个元素"]
    10. my-vec 的值并没有发生改变。

    注意,conj 函数并不是把元素添加到末尾的位置,它只能保证以最快的速度添加进一种数据结构。

    对于 vector 来说是末尾,但是对 list 来说是头部:

    1. => (def my-list '("第一个元素" "第二个元素"))
    2. #'my-clojure-study.core/my-list
    3. => (conj my-list "第三个元素")
    4. ("第三个元素" "第一个元素" "第二个元素")
    5. => my-list
    6. ("第一个元素" "第二个元素")
    7. my-list 的值同样没有发生改变。

    使用 into 函数来把两个集合进行合并。

    conj 函数一样,它只保证以最快的速度合并。

    1. => (def vec-1 ["一号元素"])
    2. #'my-clojure-study.core/vec-1
    3. => (def vec-2 ["二号元素" 3])
    4. #'my-clojure-study.core/vec-2
    5.  
    6. => (into vec-1 vec-2)
    7. ["一号元素" "二号元素" 3]
    8.  
    9. list 的情况
    10. => (def list-1 '("first"))
    11. #'my-clojure-study.core/list-1
    12. => (def list-2 '("second"))
    13. #'my-clojure-study.core/list-2
    14.  
    15. => (into list-1 list-2)
    16. ("second" "first")

    如果你接触过其它“可变的”编程语言,那么你需要提醒自己,使用 def 定义的不是变量,而是一个不可变值。

    而当你真的需要在两个操作之间交换一些信息,比如在多线程中共享一些资源,那么 Clojure 单独提供了一些类型。我们在以后的多线程部分再做详细讲解。

    [1]: 即,没有副作用的函数。 ↩