Clojure中解构Map作为函数参数:Map是否必须放在最后?
问题情况
我想定义一个接收Map和常规参数的函数,对Map的部分内容做解构,最初写的函数是:
(defn do-stuff [[{:keys [foo bar]} where] what] (println foo bar what))
调用的时候直接报错:
; Execution error (UnsupportedOperationException) at .../do-stuff (REPL:34). ; nth not supported on this type: PersistentArrayMap
要是把参数顺序交换,改成下面这样,调用时就不会报错:
(defn do-stuff [what [{:keys [foo bar]} where]] (println foo bar what))
我期望的调用方式是:
(do-stuff {:foo "something" :bar "something else"} "baz")
当然可以用函数内部的let来解构,但想搞清楚哪里错了。
错误根源
Clojure里参数列表里的[]是用来解构序列(sequential)类型的(比如列表、向量、字符串),而你第一个参数是Map(PersistentArrayMap),它属于关联式集合,不是序列。当你用[[{:keys [foo bar]} where]]去包裹第一个参数时,Clojure会尝试把这个Map当成序列来处理,调用nth方法取元素,但Map根本不支持nth操作,所以直接抛出错误。
至于交换参数后没报错,那只是个假象:此时第一个参数是字符串"baz",它属于序列,[{:keys ...} where]会把字符串当成序列,取第一个元素"b",然后尝试把"b"当成Map解构:foo和:bar,结果就是foo和bar都是nil,只是没触发类型错误而已,完全不符合你想要解构Map的需求。
正确写法
如果要在参数列表里直接解构Map,根本不需要额外的[],直接用:keys语法就行:
(defn do-stuff [{:keys [foo bar]} what] (println foo bar what))
这样调用(do-stuff {:foo "something" :bar "something else"} "baz")就会正常输出something something else baz。
如果还需要把整个Map绑定到一个变量(比如你之前写的where),可以用:as关键字:
(defn do-stuff [{:keys [foo bar] :as where} what] (println foo bar what where))
这样where就会指向传入的整个Map,同时也能直接用foo和bar。
内容的提问来源于stack exchange,提问作者agnul

