Clojure之解构

jopen 9年前

Clojure之解构

2016.01.14 09:52:29

这篇算是比较完整的Clojure解构用法文章了。

Clojure是一门Lisp方言——确切地说,是一门JVM上的Lisp方言——也是一门非纯粹的函数式语言。从我对Clojure的认知来看,Clojure的掌握成本并不低,但也不算太高。况且现在说自己简单的编程语言,深究起来有哪一门是简单的呢?Python的坑其实也蛮多的嘛?!

Clojure理所当然地秉承了Lisp“代码即数据( code is data! )”的设计哲学,直接面向抽象语法树( abstract syntax tree ,AST)。该特性正是让无数熟谙其它语言模式的开发者难以跨越的一道门槛。但无可否认的是,Lisp、Clojure中这个独树一帜的著名特性,内含了无穷无尽魔法威力,并通过括号体现出强大的语言表现能力。同Lisp一样,Clojure为数据操作的方便性提供了众多支撑手段, 解构Destructuring )特性就是其中之一,为提高编程效率助力不少。

一. 解构的提出

先来看一个例子,一个Vector: ["Mary" "Vivian" "David"] ,可以按如下方式取值:

(let [v ["Mary" "Vivian" "David"]]    (vector (first v) (second v) (last v) (nth v 1) (nth v 2)))

看到了吧?各种的 first 、 second 、 last 、 nth 在大量使用的情况下,可想而知是如何繁琐。

Python里可以这样:

mary, vivian, david= ["Mary", "Vivian", "David"]

那么,Clojure中是否也有类似的简洁而高效的Hack用法呢?

当然。这个特性在Clojure中称为 解构Destructuring ,Python、Ruby中称为 Unpacking ):

(let [[mary vivian david] ["Mary" "Vivian" "David"]]    (vector mary vivian david))

解构涉及的另一个核心概念是 绑定binding ),如mary绑定到 ["Mary" "Vivian" "David"] 的"Mary"处位置,绑定概念是解构特性的基础。

实际上,Clojure的解构特性会来得更加强大而丰富,堪称摸金校尉手中的飞虎爪,最是擅长“隔空取物”。接下来就来系统了解一下。

二. 解构特性

2.1 序列集合的解构

1. 嵌套序列解构

除了上面提到的最基本示例,Clojure的解构同时还支持嵌套序列的处理:

(let [[mary vivian david [foo1 foo2 [bar1 bar2]]] ["Mary" "Vivian" "David" ["foo1" "foo2" ["bar1" "bar2"]]]]    (vector mary vivian david [foo1 foo2 [bar1 bar2]]))

得到结果:

["Mary" "Vivian" "David" ["foo1" "foo2" ["bar1" "bar2"]]]

2. 序列分解方式

如果我们只关心解构序列中的某部分内容,可以考虑以下方式:

(let [[mary _ _ [foo1 _ [bar1 _]]] ["Mary" "Vivian" "David" ["foo1" "foo2" ["bar1" "bar2"]]]]    (vector mary [foo1 [bar1]]))

结果为:

["Mary" ["foo1" ["bar1"]]]

这时候,只获取非下划线 “_” 位置上符号绑定的内容(下划线 “_” 在这里代表的意思是:“这个位置上确实有东西,但是是什么东西我不关心,你尽管把东西放到这个位置上”)。这种分而待之的方式进一步增强了Clojure解构特性的能力。

此外,还可以通过 “&” 将序列分解为前部分与剩余部分:

(let [[mary _ david & more] ["Mary" "Vivian" "David" ["foo1" "foo2" ["bar1" "bar2"]]]]    (println more))

输出如下: ([foo1 foo2 [bar1 bar2]])

3. 保留原始内容

有时候我们还希望保留原始的序列集合,这时候应该怎么办呢?

(let [[mary _ david :as original] ["Mary" "Vivian" "David" ["foo1" "foo2" ["bar1" "bar2"]]]]    (println "mary:" mary "," "original:" original))

我们可以通过关键字 :as 来将原始内容绑定到本地符号上。结果:

mary: Mary , original: [Mary Vivian David [foo1 foo2 [bar1 bar2]]]

4. 字符串解构

解构对于字符串同样合适,这也大大方便了字符串的操作:

(let [[initial] "Mary"]    (println "initial:" initial))

结果:

initial: M

上述我们的示例大部分针对的是Vector的解构,但实际上,解构对整个序列集合也是适用的:

(let [[mary vivian david] '("Mary" "Vivian" "David")]    (vector mary vivian david))

2.2 Map解构

毫无疑问,Map在开发过程中是应用较多的数据结构,Clojure赋予Map的解构特性,使得Map的发挥如虎添翼。

(let [{mary :mary, vivian :vivian, david :david}        {:mary "Mary", :vivian "Vivian", :david "David"}]    (println mary vivian david))

这种方式虽然是解构,但一点也没提高我们的效率,还得写不少累赘的东西。Clojure提供了贴心的键与关键字映射的方式,我们可以通过它来进一步简化我们的代码。

1. 键与关键字映射

我们把上述例子改一改:

(let [{:keys [mary vivian david]}        {:mary "Mary", :vivian "Vivian", :david "David"}]    (println mary vivian david))

输出结果:

Mary Vivian David

Clojure提供了关键字 :keys ,用来在解构过程中聪明地将提取符号与原始Map的键一一对应起来,这样我们就少写了许多代码,在原始Map很复杂的情况下,无疑将大大提高开发效率。

除了关键字 :keys ,Clojure还提供了其他关键字: :strs :syms ,用于对应Map中不同的Key类型。事实上,这是一种快捷方式。我们再来看个例子:

(let [{:keys [mary vivian david], :strs [changkong], :syms [luohan]}        {:mary "Mary", :vivian "Vivian", :david "David", 'luohan "少林罗汉拳", "changkong" "长空剑法"}]    (println mary vivian david luohan changkong))

输出结果:

Mary Vivian David 少林罗汉拳 长空剑法

此外在这里要注意,绑定中的方括号 “[]” 不可少,即使只有一个绑定的符号,如: :strs [changkong] 。同时,绑定符号的字面也要跟原Map中Key类型(关键字、字符串或符号)字面值一致,如 :syms [luohan] 中的 luohan ,跟Map中的 'luohan "少林罗汉拳" 的 luohan 相对应。

2. :as 与 :or

:as 在Map中同样适用。

(let [{:keys [mary vivian david], :as original}        {:mary "Mary", :vivian "Vivian", :david "David", 'luohan "少林罗汉拳", "changkong" "长空剑法"}]    (println mary vivian david original))

结果如下:

Mary Vivian David {:mary Mary, :vivian Vivian, :david David, luohan 少林罗汉拳, changkong 长空剑法}

此外,解构应用于Map的特性中还有一个关键字 :or ,用来指定绑定符号的缺省值,如果找不到绑定符号对应Map中的键时,则使用该缺省值。

(let [{:keys [mary vivian david tom] :or {tom "not found"}}        {:mary "Mary", :vivian "Vivian", :david "David"}]    (println mary vivian david tom))

输出如下:

Mary Vivian David not found

3. 复杂嵌套Map的解构

Map在日常开发中的使用是如此广泛,因此,有必要来了解一下复杂嵌套Map的解构过程。

先来看一个例子:

(let [{mary :mary, {first-name :first-name, last-name :last-name} :name}        {:mary "Mary"         :name {:first-name "Tom", :last-name "Hanks"}}]    (format "%s和%s.%s。" mary first-name last-name))

这段代码解构了一个嵌套的Map:name,结果如下:

"Mary和Tom.Hanks。"

再稍微深入一点,如果我们想结合 :keys :strs :syms 来操作嵌套Map,要如何操作?哈哈哈,你可以停下来想一想。嗯哼,我们可以这样操作(以 :keys 为例子):

(let [{:keys [mary name]} {:mary "Mary"                             :vivian "Vivian"                             :name {:first-name "Tom", :last-name "Hanks"}}        {:keys [first-name last-name]} name]    (println (format "%s和%s.%s。" mary first-name last-name)))

结果跟上面是一样的。当然,我觉得用 :keys 的方式会更优雅一些,但这也要看个人口味。

4. Map解构与Vector、字符串

Map解构也可应用于Vector、字符串。此时,Vector、字符串的下标索引可以在Map解构形式( Form )中作为其自身的Key来使用。

(let [{c0 0, c1 1} "Hello, Clojure."]    (println (format "c1和c2分别为:%c、%c。" c0 c1)))

结果:

c1和c2分别为:H、e。

对Vector的操作也是一样。但有一点需要特别点出,那就是可以通过 & 获取Vector的剩余部分,并在剩余部分元素个数为偶数时,Map解构特性会自动将剩余部分当作Map来看待!这个实现其实并不新奇,原理跟 array-map 和 assoc 是一样的,来看看:

(let [[m v luohan & {:syms [changkong]}] ["Mary" "Vivian" "少林罗汉拳" 'changkong "长空剑法"]]    (format "%s和%s使的是%s、%s。" m v changkong luohan))

结果如下:

"Mary和Vivian使的是长空剑法、少林罗汉拳。"

我们不妨把"Vivian"放到剩余部分,看看会发生什么:

(let [[m & {:strs [Vivian], :syms [changkong]}] ["Mary" "Vivian" "少林罗汉拳" 'changkong "长空剑法"]]    (println (format "Vivian变成了%s。长空剑法还是%s。" Vivian, changkong)))

看看输出内容:

Vivian变成了少林罗汉拳。长空剑法还是长空剑法。

这种感觉真有点难以名状?

5. 其他

自Clojure 1.6起,可以在Map解构形式中使用带前缀的Map Key(中文亮了):

(let [{:keys [拳法/luohan 剑法/changkong]} {:拳法/luohan "少林罗汉拳", :剑法/changkong "长空剑法"}]    (println (format "%s,%s" luohan changkong)))

输出:

少林罗汉拳,长空剑法

三. 解构特性的应用

解构特性于Clojure的函数与宏定义而言意义重大。我们先来看看 解构在函数方面的示例

(defn kong-fu    [name school & {:keys [quan jian]}]      (printf "%s来自%s,以%s、%s闻名。\n" name school quan jian))    (kong-fu "胡八儿" "华山派" :quan "少林罗汉拳", :jian "长空剑法")

结果:

胡八儿来自华山派,以少林罗汉拳、长空剑法闻名。

在 kong-fu 函数中,参数使用到Map解构,并利用了前面提到的:通过 & 获取剩余部分,当剩余部分元素为偶数个时,将剩余部分当作Map来看待的特性。

再来看个不定长参数的例子:

(defn variable-params    [& more]    (printf "参数为:%s。\n" more))    (variable-params 1 2 3 4 5 "Mary" "Vivian" "David")  (variable-params)  (variable-params 1 10 120)

输出结果:

参数为:(1 2 3 4 5 "Mary" "Vivian" "David")。  参数为:null。  参数为:(1 10 120)。

在这里,我们把参数统一设置为剩余部分,这样就达到了不定长参数的目的,然后在函数中还可以对参数进一步解构。

接下来,我们再来了解一下强大的 宏定义中解构的应用 。实现一个 unless 函数:如果测试条件为false,则执行代码体。

(defmacro unless    "如果test为false,则执行代码体。"    {:added "1.0"}    [test & body]    (list 'if-not test (cons 'do body)))    (unless true (println "Hello, Clojure.") (println "这是一个,下雨的季节..."))  (unless false (println "Hello, Clojure.") (println "这是一个,下雨的季节..."))  (unless (= "下雨" "出太阳") (println "Hello, Clojure.") (println "这是一个,下雨的季节..."))

输出结果如下:

nil    Hello, Clojure.  这是一个,下雨的季节...    Hello, Clojure.  这是一个,下雨的季节...

在宏定义里,我们在参数中设定一个固定参数作为条件判断参数,解构出来的剩余参数部分全部作为代码体。Clojure的宏异常强大,这个 unless 仅仅是小试牛刀而已。而且本篇文章的内容主要讲解Clojure的解构特性,因此,对于宏的概念就无法过多涉及,希望后续能抽出时间再另行整理。

四. 结束

通过系统介绍强大的Clojure解构特性,相信大家领略到了Clojure的魅力和威力。Clojure经过若干年的发展与完善,得到了实践的检验,已经积累了一定的成熟度,可作为Java语言之外的另一个选择。而对于编程语言,可谓是“萝卜青菜各有所爱”,只要能够帮助你有效解决问题,就是你的压箱利器。

</div>

来自: http://www.2gua.info/post/52