• 绑定与解构
    • 绑定
      • 这里我们简单提一下 Clojure 的命名规范
    • 下面我们介绍的 let 函数,它被称为“局部绑定”
      • 我们来观察更多的例子
    • 解构
      • 解构的具体用法十分简单
      • 给原集合取名
        • 使用 :as 来给你的原集合取名

    绑定与解构

    是时候给我们的值取个名字了!

    绑定

     
    在之前的学习中,我们学会了如何使用简单的数据结构 —- listvector,但是每次使用的时候,我们都需要重新写一遍我们的元素内容,使用起来非常麻烦。

    于是 Clojure 提供了“取名”的功能:def 函数 (即 define,定义),def 函数的第一个参数是你想给你的值取的名字,第二个参数是你的值

    1. => (def my-items ["短剑" "火把" "汽油"])
    2. #'user/my-items

    这样以来我们就可以更为方便的使用我们的 vector

    1. => (first my-items)
    2. "短剑"

    (注意,def 所定义的内容不是变量,会在之后的文章中讨论这一问题)

    我们观察它的返回值 #'user/my-items 它表示我们成功地在 user 空间中创建了一个 my-itemsuser 空间是 REPL 启动后默认的命名空间(namespace),它表示我们的 my-items 只在 user 中才能使用。

    (命名空间的作用和用法,日后再说…现在你可以把它理解为一个文件夹,我们是在 user 这个“文件夹”下面新建了一个 my-items,如果你去另外的文件夹里面访问,即使那个文件夹也有一个叫 my-items 的东西,那它也不是我们这个 my-items)

    这里我们简单提一下 Clojure 的命名规范

    我们使用全小写字母和减号间隔来表示 Clojure 里的一个值的名称

    如:

    a-beautiful-world

    items

    lovely-girl

    使用 def 函数定义一个值,这个值在整个 user 空间都可以被访问

    下面我们介绍的 let 函数,它被称为“局部绑定”

    (在其它文章中也称之为“本地绑定”、“本地值”、“let 绑定”,指的都是我们这里介绍的内容)

    之所以叫它“局部绑定”,是因为使用它来命名的名称,只在 let 的括号范围内才能使用。

    1. => (let [my-items ["短剑" "火把" "汽油"]]
    2. (println my-items))
    3. ["短剑" "火把" "汽油"]
    4. nil

    我们在第二和第三个参数之间换行,这并不影响程序的执行,无论你是否换行,但这增加了程序的可读性 [1] 。

    养成良好的编程风格,适当的缩进和换行会增加程序的可读性

    • let 函数的使用比我们之前所学的函数要复杂一点 *

    首先,它的第一个参数是一个 vector (使用中括号包围),中括号里元素个数必须为偶数,因为中括号里每一对元素表示“给值取的名字” - “值”(key-value),也就是说,我们可以一次性地给多个值取名字。

    然后,它之后的参数是多个表达式,表达式会被 Clojure 依次执行。

    最后, let 函数的返回值等于它最后执行的表达式的值。

    我们来观察更多的例子

    1. => (let [my-items ["短剑" "火把" "汽油"] my-coins [128]]
    2. (print my-coins)
    3. my-items)
    4. [128]
    5. ["短剑" "火把" "汽油"]
    1. => (let [my-items ["短剑" "火把" "汽油"] my-coins [128]]
    2. my-items
    3. my-coins
    4. (print my-coins))
    5. [128]
    6. nil
    1. => (let [my-items ["短剑" "火把" "汽油"] my-coins [128]]
    2. my-items
    3. my-coins
    4. (print my-coins))
    5. (print my-coins)
    6. [128]
    7. nil
    8. CompilerException java.lang.RuntimeException: Unable to resolve symbol: my-coins in this context

    第一段代码展示了如何使用 let 来一次性地定义两个值 —- my-itemsmy-coins,然后在 let 的括号范围内,我们依次执行了两句表达式,而且我们在表达式之间使用换行以增加可读性。

    第一句表达式输出 my-coins 的值

    第二句表达式直接使用 my-items,没有使用括号包围,这表示我们直接使用它的值,而不是把它作为函数

    由于 let 函数规定最后被执行的表达式的值为 let 函数的返回值,所以此例中 let 函数的返回值为 my-items 的值

    而第二段代码与第一段不同的是,它最后才执行了 print 函数

    我们可以看到, print 函数的副作用效果出现 —- my-coins 的值 [128] 被打印出来,但是由于 print 函数的返回值始终为 nil

    所以此例中 let 函数的返回值为最后执行的表达式的值 —- 即 print 函数的值,虽然在它之前我们对 my-itemsmy-coins 的值进行的访问,但由于没有执行打印到屏幕上的操作,我们无法观察到它们的值

    在第三段代码中,我们尝试在 let 函数的括号外访问 my-coins,结果可想而知,错误信息表示:Unable to resolve symbol: my-coins (无法理解符号 my-coins)。

    因为 let 函数给值取的名字的有效性只在它的“势力范围”之内,即只能在它前后括号的范围内使用。

    很多函数隐式地使用了 let,在今后的 “定义属于你自己的函数” 章节中,这里所学习到的有关 let 的知识就能够派上更大的用场了

    解构

     
    在前一个章节中,我们学习了如何访问一个集合中的元素,但如果每次都这样使用,显得繁琐而无聊。

    1. => (def my-items ["短剑" "火把" "汽油"])
    2. #'user/my-items
    3. => (first my-items)
    4. 短剑
    5. => (rest my-items)
    6. ("火把" "汽油") ;复习一下,rest 函数返回除 first 之外剩余元素的 list 形式

    尤其是我们想给一个集合里面的元素都取一个新名字时

    1. =>(def first-item (first my-items))
    2. #'user/first-item
    3. =>(def rest-item (rest my-items))
    4. #'user/rest-item

    或者在访问一个多层嵌套的集合时

    1. => (def my-coins 256)
    2. #'user/my-coins
    3. => (def my-bag [my-items my-coin])
    4. #'user/my-bag
    5. => (nth (first my-bag) 2)
    6. 汽油

    可以想象如果一个集合里面的元素非常多,或者嵌套层数非常多的时候,这种方式效率十分低下。

    不过,当你的嵌套层数非常多的时候,就该反思一下你的设计了。

    (可能写出来的代码你自己也读不懂)

    幸亏 let 函数给我们提供了一个诱人的访问集合元素的方式

    1. => (def my-bag [["短剑" "火把" "汽油"] 256])
    2. #'user/my-bag
    3. => (let [[items coins] my-bag]
    4. (println "你所拥有的装备:" items) ;复习一下,print 函数家族可以接受多个参数,并依次输出他们的值
    5. (println "你所拥有的金币:" coins))
    6. 你所拥有的装备: [短剑 火把 汽油]
    7. 你所拥有的金币: 256
    8. nil

    我们称之为 解构,因为它可以清晰地表现原结构的样子。

    如果不使用解构,就会长成这样:

    1. => (let [items (first my-bag) coins (second my-bag)]
    2. (println "你所拥有的装备:" items)
    3. (println "你所拥有的金币:" coins))
    4. 你所拥有的装备: [短剑 火把 汽油]
    5. 你所拥有的金币: 256
    6. nil

    解构的具体用法十分简单

    你只需要,在 let 函数的第二个参数里,把原来 “给值取的名字” 的位置,写成一个 vector 的形式(即用中括号包围),原来填写 “值” 的位置,写上你要解构的集合。

    (let [[你给集合里元素取的新名字] 你要解构的集合])

    如果我们把原结构和解构形式放在一起观察的话

    1. my-bag [["短剑" "火把" "汽油"] 256 ]
    2. [ items coins] my-bag

    看起来像是把定义集合倒过来写一样,这样我们就在 let 绑定里面给这个两个元素取了一个名字:

    • 集合 my-bag 的第一个元素取名为 items
    • 集合 my-bag 的第二个元素取名为 coins

      而且,这个对应关系真实反映了元素的位置

    我们可以使用解构从集合中取部分元素

    1. => (def my-bag [["短剑" "火把" "汽油"] 512 2])
    2. #'user/my-bag
    3. => (let [[items silver-coin] my-bag]
    4. (println "你所拥有的装备:" items)
    5. (println "你所拥有的银币:" silver-coin))
    6. 你所拥有的装备: [短剑 火把 汽油]
    7. 你所拥有的银币: 512
    8. nil

    上面的例子中,虽然我们的 my-bag 有三个元素,但是解构可以只拿取前两个。

    看起来就像这样

    1. my-bag [ ["短剑" "火把" "汽油"] 512 2]
    2. [ items silver-coin ] my-bag

    如果你只想要物品和金币,你可以这样来操作

    1. => (let [[items silver-coin gold-coin] my-bag]
    2. (println "你所拥有的装备:" items)
    3. (println "你所拥有的金币:" gold-coin))
    4. 你所拥有的装备: [短剑 火把 汽油]
    5. 你所拥有的金币: 2
    6. nil

    是不是看起来有点傻,给它取了名字却没有使用。

    不过,如果我们不关心银币,那么我们可以给它随便扔一个名字,比如 sth-I-don't-care,不过 Clojure 规范更倾向于使用短下划线 _ 来命名你不感兴趣的名称。

    1. => (let [[items _ gold-coin] my-bag]
    2. (println "你所拥有的装备:" items)
    3. (println "你所拥有的金币:" gold-coin))
    4. 你所拥有的装备: [短剑 火把 汽油]
    5. 你所拥有的金币: 2
    6. nil

    其实它只也是一个可以正常使用的名字而已

    1. => (let [[items _ gold-coin] my-bag]
    2. (println "你所拥有的装备:" items)
    3. (println "你所拥有的银币:" _)
    4. (println "你所拥有的金币:" gold-coin))
    5. 你所拥有的装备: [短剑 火把 汽油]
    6. 你所拥有的银币: 512
    7. 你所拥有的金币: 2
    8. nil

    如果你使用重复的名字,比如使用多个 _ 来忽视掉你不关心的内容,那么后面的值会把前面的值覆盖掉。

    1. => (let [[items _ _] my-bag]
    2. (println "你所拥有的装备:" items)
    3. (println "你所拥有的银币:" _)
    4. (println "你所拥有的金币:" _))
    5. 你所拥有的装备: [短剑 火把 汽油]
    6. 你所拥有的银币: 2 ;这里金币的值覆盖了银币的值
    7. 你所拥有的金币: 2
    8. nil

    在解构多层嵌套时,更能发挥出它的威力

    1. => (def my-bag [["短剑" "火把" "汽油"] [512 2]])
    2. => (let [[[first-item _ third-item] [silver-coin gold-coin]] my-bag]
    3. (println "你背包里的第一件装备:" first-item)
    4. (println "你背包里的第三件装备:" third-item)
    5. (println "你所拥有的金币:" gold-coin)
    6. (println "你所拥有的银币:" silver-coin))
    7. 你背包里的第一件装备: 短剑
    8. 你背包里的第三件装备: 汽油
    9. 你所拥有的金币: 2
    10. 你所拥有的银币: 512
    11. nil

    我们像之前一样对比一下原集合和解构形式

    1. my-bag [[ "短剑" "火把" "汽油" ] [ 512 2 ] ]
    2. [[first-item _ third-item] [silver-coin gold-coin] ] my-bag

    这也是我们为什么说,它可以清晰地表现原结构的样子。

    解构的额外特性,给剩余元素取名。

    1. => (def my-bag [["短剑" "火把" "汽油"] 512 2])
    2. => (let [[[first-item _ third-item] & coins] my-bag]
    3. (println "你背包里的第一件装备:" first-item)
    4. (println "你背包里的第三件装备:" third-item)
    5. (println "你所拥有的钱币:" coins))
    6. 你背包里的第一件装备: 短剑
    7. 你背包里的第三件装备: 汽油
    8. 你所拥有的钱币: (512 2)
    9. nil

    使用 & 来把剩余元素作为一个 list 绑定到一个名字,在做递归调用的时候这个功能用起来就太爽了。

    给原集合取名

    使用 :as 来给你的原集合取名

    :as 是一个 key (key 会在今后的介绍中出现)

    1. => (let [[[first-item _ third-item] & coins :as my-bag-original] my-bag]
    2. (println "你背包里的第一件装备:" first-item)
    3. (println "你背包里的第三件装备:" third-item)
    4. (println "你所拥有的钱币:" coins)
    5. (println "全部物品:" my-bag-original))
    6. 你背包里的第一件装备: 短剑
    7. 你背包里的第三件装备: 汽油
    8. 你所拥有的钱币: (512 2)
    9. 全部物品: [[短剑 火把 汽油] 512 2]
    10. nil

    本文所介绍的解构称为顺序解构,它可以用来对顺序集合做解构,包括:

    listvector

    实现了 java.unit.List 接口的集合

    Java 数组

    字符串

    对字符串的解构结果是一个一个字符

    1. => (let [[f s t] "123"]
    2. (print s))
    3. 2
    4. nil

    还有为 map 服务的解构形式,在今后对 map 做单独介绍时再详细说明

    [1]: 可读性,指人类阅读程序语言时的“舒适程度”,“易于理解程度”。读起来更容易被理解的程序的可读性就越高 ↩