- 绑定与解构
- 绑定
- 这里我们简单提一下
Clojure的命名规范
- 这里我们简单提一下
- 下面我们介绍的
let函数,它被称为“局部绑定”- 我们来观察更多的例子
- 解构
- 解构的具体用法十分简单
- 给原集合取名
- 使用 :as 来给你的原集合取名
- 使用 :as 来给你的原集合取名
- 绑定
绑定与解构
是时候给我们的值取个名字了!
绑定
在之前的学习中,我们学会了如何使用简单的数据结构 —- list 和 vector,但是每次使用的时候,我们都需要重新写一遍我们的元素内容,使用起来非常麻烦。
于是 Clojure 提供了“取名”的功能:def 函数 (即 define,定义),def 函数的第一个参数是你想给你的值取的名字,第二个参数是你的值
=> (def my-items ["短剑" "火把" "汽油"])#'user/my-items
这样以来我们就可以更为方便的使用我们的 vector 了
=> (first my-items)"短剑"
(注意,def 所定义的内容不是变量,会在之后的文章中讨论这一问题)
我们观察它的返回值 #'user/my-items 它表示我们成功地在 user 空间中创建了一个 my-items,user 空间是 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 的括号范围内才能使用。
=> (let [my-items ["短剑" "火把" "汽油"]](println my-items))["短剑" "火把" "汽油"]nil
我们在第二和第三个参数之间换行,这并不影响程序的执行,无论你是否换行,但这增加了程序的可读性 [1] 。
养成良好的编程风格,适当的缩进和换行会增加程序的可读性
let函数的使用比我们之前所学的函数要复杂一点 *
首先,它的第一个参数是一个 vector (使用中括号包围),中括号里元素个数必须为偶数,因为中括号里每一对元素表示“给值取的名字” - “值”(key-value),也就是说,我们可以一次性地给多个值取名字。
然后,它之后的参数是多个表达式,表达式会被 Clojure 依次执行。
最后, let 函数的返回值等于它最后执行的表达式的值。
我们来观察更多的例子
=> (let [my-items ["短剑" "火把" "汽油"] my-coins [128]](print my-coins)my-items)[128]["短剑" "火把" "汽油"]
=> (let [my-items ["短剑" "火把" "汽油"] my-coins [128]]my-itemsmy-coins(print my-coins))[128]nil
=> (let [my-items ["短剑" "火把" "汽油"] my-coins [128]]my-itemsmy-coins(print my-coins))(print my-coins)[128]nilCompilerException java.lang.RuntimeException: Unable to resolve symbol: my-coins in this context
第一段代码展示了如何使用 let 来一次性地定义两个值 —- my-items 和 my-coins,然后在 let 的括号范围内,我们依次执行了两句表达式,而且我们在表达式之间使用换行以增加可读性。
第一句表达式输出 my-coins 的值
第二句表达式直接使用 my-items,没有使用括号包围,这表示我们直接使用它的值,而不是把它作为函数
由于
let函数规定最后被执行的表达式的值为let函数的返回值,所以此例中let函数的返回值为my-items的值而第二段代码与第一段不同的是,它最后才执行了
我们可以看到,
my-coins的值[128]被打印出来,但是由于nil。所以此例中
let函数的返回值为最后执行的表达式的值 —- 即my-items和my-coins的值进行的访问,但由于没有执行打印到屏幕上的操作,我们无法观察到它们的值
在第三段代码中,我们尝试在 let 函数的括号外访问 my-coins,结果可想而知,错误信息表示:Unable to resolve symbol: my-coins (无法理解符号 my-coins)。
因为 let 函数给值取的名字的有效性只在它的“势力范围”之内,即只能在它前后括号的范围内使用。
很多函数隐式地使用了 let,在今后的 “定义属于你自己的函数” 章节中,这里所学习到的有关 let 的知识就能够派上更大的用场了
解构
在前一个章节中,我们学习了如何访问一个集合中的元素,但如果每次都这样使用,显得繁琐而无聊。
=> (def my-items ["短剑" "火把" "汽油"])#'user/my-items=> (first my-items)短剑=> (rest my-items)("火把" "汽油") ;复习一下,rest 函数返回除 first 之外剩余元素的 list 形式
尤其是我们想给一个集合里面的元素都取一个新名字时
=>(def first-item (first my-items))#'user/first-item=>(def rest-item (rest my-items))#'user/rest-item
或者在访问一个多层嵌套的集合时
=> (def my-coins 256)#'user/my-coins=> (def my-bag [my-items my-coin])#'user/my-bag=> (nth (first my-bag) 2)汽油
可以想象如果一个集合里面的元素非常多,或者嵌套层数非常多的时候,这种方式效率十分低下。
不过,当你的嵌套层数非常多的时候,就该反思一下你的设计了。
(可能写出来的代码你自己也读不懂)
幸亏 let 函数给我们提供了一个诱人的访问集合元素的方式
=> (def my-bag [["短剑" "火把" "汽油"] 256])#'user/my-bag=> (let [[items coins] my-bag](println "你所拥有的装备:" items) ;复习一下,print 函数家族可以接受多个参数,并依次输出他们的值(println "你所拥有的金币:" coins))你所拥有的装备: [短剑 火把 汽油]你所拥有的金币: 256nil
我们称之为 解构,因为它可以清晰地表现原结构的样子。
如果不使用解构,就会长成这样:
=> (let [items (first my-bag) coins (second my-bag)](println "你所拥有的装备:" items)(println "你所拥有的金币:" coins))你所拥有的装备: [短剑 火把 汽油]你所拥有的金币: 256nil
解构的具体用法十分简单
你只需要,在 let 函数的第二个参数里,把原来 “给值取的名字” 的位置,写成一个 vector 的形式(即用中括号包围),原来填写 “值” 的位置,写上你要解构的集合。
(let [[你给集合里元素取的新名字] 你要解构的集合])
如果我们把原结构和解构形式放在一起观察的话
my-bag [["短剑" "火把" "汽油"] 256 ][ items coins] my-bag
看起来像是把定义集合倒过来写一样,这样我们就在 let 绑定里面给这个两个元素取了一个名字:
- 集合 my-bag 的第一个元素取名为 items
- 集合 my-bag 的第二个元素取名为 coins
而且,这个对应关系真实反映了元素的位置
我们可以使用解构从集合中取部分元素
=> (def my-bag [["短剑" "火把" "汽油"] 512 2])#'user/my-bag=> (let [[items silver-coin] my-bag](println "你所拥有的装备:" items)(println "你所拥有的银币:" silver-coin))你所拥有的装备: [短剑 火把 汽油]你所拥有的银币: 512nil
上面的例子中,虽然我们的 my-bag 有三个元素,但是解构可以只拿取前两个。
看起来就像这样
my-bag [ ["短剑" "火把" "汽油"] 512 2][ items silver-coin ] my-bag
如果你只想要物品和金币,你可以这样来操作
=> (let [[items silver-coin gold-coin] my-bag](println "你所拥有的装备:" items)(println "你所拥有的金币:" gold-coin))你所拥有的装备: [短剑 火把 汽油]你所拥有的金币: 2nil
是不是看起来有点傻,给它取了名字却没有使用。
不过,如果我们不关心银币,那么我们可以给它随便扔一个名字,比如 sth-I-don't-care,不过 Clojure 规范更倾向于使用短下划线 _ 来命名你不感兴趣的名称。
=> (let [[items _ gold-coin] my-bag](println "你所拥有的装备:" items)(println "你所拥有的金币:" gold-coin))你所拥有的装备: [短剑 火把 汽油]你所拥有的金币: 2nil
其实它只也是一个可以正常使用的名字而已
=> (let [[items _ gold-coin] my-bag](println "你所拥有的装备:" items)(println "你所拥有的银币:" _)(println "你所拥有的金币:" gold-coin))你所拥有的装备: [短剑 火把 汽油]你所拥有的银币: 512你所拥有的金币: 2nil
如果你使用重复的名字,比如使用多个 _ 来忽视掉你不关心的内容,那么后面的值会把前面的值覆盖掉。
=> (let [[items _ _] my-bag](println "你所拥有的装备:" items)(println "你所拥有的银币:" _)(println "你所拥有的金币:" _))你所拥有的装备: [短剑 火把 汽油]你所拥有的银币: 2 ;这里金币的值覆盖了银币的值你所拥有的金币: 2nil
在解构多层嵌套时,更能发挥出它的威力
=> (def my-bag [["短剑" "火把" "汽油"] [512 2]])=> (let [[[first-item _ third-item] [silver-coin gold-coin]] my-bag](println "你背包里的第一件装备:" first-item)(println "你背包里的第三件装备:" third-item)(println "你所拥有的金币:" gold-coin)(println "你所拥有的银币:" silver-coin))你背包里的第一件装备: 短剑你背包里的第三件装备: 汽油你所拥有的金币: 2你所拥有的银币: 512nil
我们像之前一样对比一下原集合和解构形式
my-bag [[ "短剑" "火把" "汽油" ] [ 512 2 ] ][[first-item _ third-item] [silver-coin gold-coin] ] my-bag
这也是我们为什么说,它可以清晰地表现原结构的样子。
解构的额外特性,给剩余元素取名。
=> (def my-bag [["短剑" "火把" "汽油"] 512 2])=> (let [[[first-item _ third-item] & coins] my-bag](println "你背包里的第一件装备:" first-item)(println "你背包里的第三件装备:" third-item)(println "你所拥有的钱币:" coins))你背包里的第一件装备: 短剑你背包里的第三件装备: 汽油你所拥有的钱币: (512 2)nil
使用 & 来把剩余元素作为一个 list 绑定到一个名字,在做递归调用的时候这个功能用起来就太爽了。
给原集合取名
使用 :as 来给你的原集合取名
:as 是一个 key (key 会在今后的介绍中出现)
=> (let [[[first-item _ third-item] & coins :as my-bag-original] my-bag](println "你背包里的第一件装备:" first-item)(println "你背包里的第三件装备:" third-item)(println "你所拥有的钱币:" coins)(println "全部物品:" my-bag-original))你背包里的第一件装备: 短剑你背包里的第三件装备: 汽油你所拥有的钱币: (512 2)全部物品: [[短剑 火把 汽油] 512 2]nil
本文所介绍的解构称为顺序解构,它可以用来对顺序集合做解构,包括:
list,vector实现了
java.unit.List接口的集合
Java数组字符串
对字符串的解构结果是一个一个字符
=> (let [[f s t] "123"](print s))2nil
还有为 map 服务的解构形式,在今后对 map 做单独介绍时再详细说明
[1]: 可读性,指人类阅读程序语言时的“舒适程度”,“易于理解程度”。读起来更容易被理解的程序的可读性就越高 ↩
