Clojure 宏 上篇

简介:
  开始接触Lisp宏是看Ansi Common Lisp的第十章,Lisp宏定义相关的话题都已经提到,有兴趣的可以看看.ACL的目前已经在Github上有中文译本 [ 第十章],不要太担心Clojure与Lisp的语法差异,可以看下面这个对照表  http://clojure.org/lisps . 
 
 
    Clojure 宏给人留下第一印象就是各种符号 ` ' ~ ~@ ,那就从这些符号怎么读开始吧
 
 

怎么读

 
复制代码
user=> (defmacro foreach [[sym coll] & body]
  `(loop [coll# ~coll]
     (when-let [[~sym & xs#] (seq coll#)]
       ~@body
       (recur xs#))))
#'user/foreach
user=>
user=> (foreach [x [1 2 3]]
  (println x))
1
2
3
nil
user=>
复制代码

 

  先看看 Clojure 官方文档是怎么称呼这几个符号的:
 
  Syntax-quote (`, note, the "backquote" character), Unquote (~) and Unquote-splicing (~@) For all forms other than Symbols, Lists, Vectors, Sets and Maps, `x is the same as 'x.
 

怎么用

 
   知道了名字,下面就要看看符号的作用了:  Syntax-quote `防止宏内部的表达式求值,宏代码体内的代码替换到使用这个宏的地方.如果仅仅代码文本的替换,灵活性就有限了,我们使用 unquote符号~进行不宏代码体内的表达式;如果symbol代表的是一个seq,那么我们可以使用 Unquote-splicing (~@) 进行seq数据项的展开.
 
看例子:
 
复制代码
user=> (defmacro dbg[x] `(let [x# ~x] (println '~x "=" x#) x#))
 
user=> (def x 5)
user=> (def lst '(a b c))
user=> `(fred x ~x lst ~@lst 7 8 :nine)
    (user/fred user/x 5 user/lst a b c 7 8 :nine)
 
user=> `(abc ~(symbol (str "i" "s" \- "cool")))
(user/abc is-cool)
 
user=>  `(max ~@(shuffle (range 10)))
(clojure.core/max 8 7 1 9 0 6 4 2 3 5)
复制代码

 

 

` '的区别

 
Clojure  ' `区别在于 Syntax-quote (`)会进行symbol的解析
 
user=> '(foo bar)
(foo bar)
user=> `(foo bar)
(user/foo user/bar)

 

 下面的代码中Syntax-quote 包含的代码中包含symbol x,而在当前的代码空间并没有user/x的定义,所以抛出了异常:
user=> (defmacro debug [x] `(println ">>" '~x ":" ~x  x))
#'user/debug
user=> (let [a 10] (debug a))
CompilerException java.lang.RuntimeException: No such var: user/x, compiling:(NO
_SOURCE_PATH:72)
user=>

 

 我们暂时把x随便换成一个数字23让代码可以执行,可以看到其它部分的代码都是正确的:
user=>  (defmacro debug [x] `(println ">>" '~x ":" ~x  23))
#'user/debug
user=> (let [a 10] (debug a))
>> a : 10 23
nil
user=> 

 

 
 

syntax-quote 嵌套

 
   syntax-quote 将symbol解析成为fully-qulified symbol,所谓fully-qulified symbol 就是形如namespace/name或fully.qualified.Classname 如果是symbol是非名称空间限定的(non-namespace-qualified)且以#符号结尾,会解析成为name_uniqueid的形式比如x_123.
 
   这里遇到一个比较郁闷的问题就是syntax-quote嵌套,这个在Shell中测试的代码和直觉并不一致,我的问题是:
 
        user=> `'y 
(quote user/y) 
这个是可以理解的,'y等价(quote y),`'y 也就是`(quote y),结果是(quote user/y) 
 
但是``y 的结果和我预期的不一致: 
user=> ``y 
(quote user/y) 
 
我想的是`y 的结果是user/y,然后`user/y的结果是user/y,也就是说``y的结果应该是user/y 
 
 
   请教了豆瓣的友邻  @huangz 得到解答:如果 syntex-quote 里面包含的是 resloved symbol ,那就简单的使用quote包围一下.这样对于上面的代码,``y结果是 (quote user/y),就可以理解了. 解答这种问题最好的方式就是看一下Reader的逻辑实现:  https://github.com/clojure/clojure/blob/master/src/jvm/clojure/lang/LispReader.java
 
复制代码
 
' 的实现
dispatchMacros['\''] = new VarReader();
 
public static class VarReader extends AFn{
     public Object invoke(Object reader, Object quote) {
          PushbackReader r = (PushbackReader) reader;
          Object o = read(r, true, null, true);
          return RT.list(THE_VAR, o);
     }
}
 
 
 
 
`的实现
macros['`'] = new SyntaxQuoteReader();
 
public static class SyntaxQuoteReader extends AFn
{
 // 代码省略........
}
 
~的实现
 
macros['~'] = new UnquoteReader();
 

static class UnquoteReader extends AFn{
     public Object invoke(Object reader, Object comma) {
          PushbackReader r = (PushbackReader) reader;
          int ch = read1(r);
          if(ch == -1)
               throw Util.runtimeException("EOF while reading character");
          if(ch == '@')
               {
               Object o = read(r, true, null, true);
               return RT.list(UNQUOTE_SPLICING, o);
               }
          else
               {
               unread(r, ch);
               Object o = read(r, true, null, true);
               return RT.list(UNQUOTE, o);
               }
     }

}
复制代码

 

  通过下面这个几乎只会在试卷中出现的代码检查一下我们对符号嵌套的理解吧,平时没有人心理扭曲到写这种无聊的代码吧
 
复制代码
user=>  (let [x 9, y '(- x)]
  (println 0 y)
  (println 1 `y)
  (println 2 ``y)
  (println 3 ```y)
  ;(println 4 ~y)
  (println 5 `~y)
  (println 6 ``~y)
  (println 7 ``~~y))
0 (- x)
1 user/y
2 (quote user/y)
3 (clojure.core/seq (clojure.core/concat (clojure.core/list (quote quote)) (clojure.core/list (quote user/y))))
5 (- x)
6 user/y
7 (- x)
nil
user=> 
复制代码

 

  其实还可以更无聊一点,定义 x y 我们后面的测试就围绕这两个变量展开:
 
复制代码
user=> (def x 12)
#'user/x
user=> (def y 23)
#'user/y
 

user=> `y  
user/y
user=> ``y  
(quote user/y)
user=> `'y
(quote user/y)
user=> ```y    
(clojure.core/seq (clojure.core/concat (clojure.core/list (quote quote)) (clojure.core/list (quote user/y))))
user=> ````y
(clojure.core/seq (clojure.core/concat (clojure.core/list (quote clojure.core/seq)) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote clojure.core/concat)) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote clojure.core/list)) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote quote)) (clojure.core/list (quote quote)))))))) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote clojure.core/list)) (clojure.core/list (clojure.core/seq (clojure.core/concat (clojure.core/list (quote quote)) (clojure.core/list (quote user/y)))))))))))))
 
  下面开始折腾~运算符,首先看到~需要在`的情况下才有效,否则就会有下面这种错误
 
user=> ~y  
IllegalStateException Attempting to call unbound fn: #'clojure.core/unquote  clojure.lang.Var$Unbound.throwArity (Var.java:43)
 

user=> `~y
23
user=> `'y
(quote user/y)
user=> 'y
y
user=> `'~y
(quote 23)
user=> `23
23
user=> `~~y
IllegalStateException Attempting to call unbound fn: #'clojure.core/unquote  clojure.lang.Var$Unbound.throwArity (Var.java:43)
user=> ``~~y
23
user=> `~'y
y
user=> (= `y 'y)
false
user=> (= y 'y)
false
user=> 'y
y
user=> (= 'y (quote y))
true
user=> (quote y)
y
user=> ``~~y
23
user=> ``~y
user/y
user=> `~y
23
user=> (macexpand-1 ``~~y)
CompilerException java.lang.RuntimeException: Unable to resolve symbol: macexpand-1 in this context, compiling:(NO_SOURCE_PATH:133)
user=> (macroexpand-1 ``~~y)
23
user=> `'23
(quote 23)
user=> 
复制代码
 
 
 

宏展开

 
 "Clojure Programming" 书中有一个Clojure编译的流程图:
 
 
 
  我们可以使用macroexpand和macroexpand-1这样个辅助方法来查看宏展开的情况,下面是我们测试的代码:   
 
复制代码
user=>  (defmacro ya-defn [fn-name args & body]
          `(defn ~fn-name ~args
           (println "Calling ..." ~fn-name ~args)
           ~@body))
#'user/ya-defn
user=> (ya-defn add [a b]
         (+ a b))
#'user/add
user=> (add 2 3)
Calling ... #<user$add user$add@d3583e> [2 3]
5
复制代码

 

   编译器进行宏展开,宏产出的代码成为原始程序的一部分.可以通过调用macroexpand-1 或者macroexpand 查看宏展开的结果,这两个函数的区别在于macroexpand会反复调用macroexpand-1进行宏展开,直到没有宏为止.下面的例子可以看到这个差别,注意为了清晰看到代码结构我在前面添加了pprint的函数调用.
 
复制代码
user=> (macroexpand-1 '(ya-defn add [a b]  (+ a b)))
(clojure.core/defn add [a b] (clojure.core/println "Calling ..." add [a b]) (+ a b))

user=> (pprint (macroexpand-1 '(ya-defn add [a b]  (+ a b))))
(clojure.core/defn
add
[a b]
(clojure.core/println "Calling ..." add [a b])
(+ a b))
nil
 
user=> (pprint (macroexpand '(ya-defn add [a b]  (+ a b))))
(def
add
(clojure.core/fn
  ([a b] (clojure.core/println "Calling ..." add [a b]) (+ a b))))
nil
复制代码

 


  下面是函数macroexpand和macroexpand-1的源码实现,代码胜千言:
 
复制代码
user=> (source macroexpand)
(defn macroexpand
  "Repeatedly calls macroexpand-1 on form until it no longer
  represents a macro form, then returns it.  Note neither
  macroexpand-1 nor macroexpand expand macros in subforms."
  {:added "1.0"
   :static true}
  [form]
    (let [ex (macroexpand-1 form)]
      (if (identical? ex form)
        form
        (macroexpand ex))))
nil
user=> (source macroexpand-1)
(defn macroexpand-1
  "If form represents a macro form, returns its expansion,
  else returns form."
  {:added "1.0"
   :static true}
  [form]
    (. clojure.lang.Compiler (macroexpand1 form)))
nil
user=>
复制代码

 

 

auto_gensym机制

 
 我们想创建一个unqualified symbol 的时候,就会在symbol的后面添加#符号.
 
复制代码
user=> `(x#)
(x__6__auto__)
 
;;;定义dbg
user=> (defmacro dbg[x] `(let [x# ~x] (println '~x "=" x#) x#))
#'user/dbg

user=> (defn pythag [x,y] (*(* x x) (* y y)))
#'user/pythag
user=> (pythag  5 6 )
900
user=> (defn pythag [x,y] (dbg(* (dbg (* x x)) (dbg (* y y)))))
#'user/pythag
user=> (pythag  5 6 )
(* x x) = 25
(* y y) = 36
(* (dbg (* x x)) (dbg (* y y))) = 900
900
复制代码

 

 

从代码中学习

 
  学习Macro,自我感觉比较好的学习方式就是看Clojure中宏的实现,尝试自己写一下.再看两个宏的例子:通过source函数查看->和 ->>的内部实现:
 
复制代码
user=> ( -> 25 Math/sqrt int list)
(5)
user=> ( ->> 25 Math/sqrt int list)
(5)

user=> (source ->)
(defmacro ->
  "Threads the expr through the forms. Inserts x as the
  second item in the first form, making a list of it if it is not a
  list already. If there are more forms, inserts the first form as the
  second item in second form, etc."
  {:added "1.0"}
  ([x] x)
  ([x form] (if (seq? form)
              (with-meta `(~(first form) ~x ~@(next form)) (meta form))
              (list form x)))
  ([x form & more] `(-> (-> ~x ~form) ~@more)))
nil
user=> (source ->>)
(defmacro ->>
  "Threads the expr through the forms. Inserts x as the
  last item in the first form, making a list of it if it is not a
  list already. If there are more forms, inserts the first form as the
  last item in second form, etc."
  {:added "1.1"}
  ([x form] (if (seq? form)
              (with-meta `(~(first form) ~@(next form)  ~x) (meta form))
              (list form x)))
  ([x form & more] `(->> (->> ~x ~form) ~@more)))
nil
user=>
复制代码

   

附 一段很实用的宏:

user=> (source2 kw) 
(defmacro kw 
   "查询当前所有ns中含特定字符串的函数,如: (kw -index)" 
   [s] `(filter #(>= (.indexOf (str %) (name '~s)) 0) 
                (sort (keys (mapcat ns-publics (all-ns)))))) 
nil 
user=> (kw source) 
(*source-path* read-resource resource source source-fn source-fn2 source2) 
user=> (kw -index) 
(keep-indexed map-indexed safe-index)
 
 
 
 
 
 
 
 
 
 
 
 

最后小图一张:
 
 日本的侦探,名气最大的当然是金田一 -无论爷爷还是孙子.  by 青山刚昌
 
 
目录
相关文章
|
6月前
|
自然语言处理 前端开发 Java
深入浅出JVM(六)之前端编译过程与语法糖原理
深入浅出JVM(六)之前端编译过程与语法糖原理
|
编译器 C语言 C++
C语言基础教程(宏的使用和多文件编程)
C语言基础教程(宏的使用和多文件编程)
215 0
|
算法 C语言
【C语言函数】入门教程
【C语言函数】入门教程
77 0
|
存储 自然语言处理 程序员
进阶C语言 第七章-------《程序的编译(预处理操作)+链接》 (预编译、编译、汇编、#define、条件编译,#include的包含)知识点+完整思维导图+基本练习题+深入细节+通俗易懂建议收藏(一)
进阶C语言 第七章-------《程序的编译(预处理操作)+链接》 (预编译、编译、汇编、#define、条件编译,#include的包含)知识点+完整思维导图+基本练习题+深入细节+通俗易懂建议收藏(一)
|
编译器 C语言
进阶C语言 第七章-------《程序的编译(预处理操作)+链接》 (预编译、编译、汇编、#define、条件编译,#include的包含)知识点+完整思维导图+基本练习题+深入细节+通俗易懂建议收藏(二)
进阶C语言 第七章-------《程序的编译(预处理操作)+链接》 (预编译、编译、汇编、#define、条件编译,#include的包含)知识点+完整思维导图+基本练习题+深入细节+通俗易懂建议收藏(二)
进阶C语言 第七章-------《程序的编译(预处理操作)+链接》 (预编译、编译、汇编、#define、条件编译,#include的包含)知识点+完整思维导图+基本练习题+深入细节+通俗易懂建议收藏(三)
进阶C语言 第七章-------《程序的编译(预处理操作)+链接》 (预编译、编译、汇编、#define、条件编译,#include的包含)知识点+完整思维导图+基本练习题+深入细节+通俗易懂建议收藏(三)
|
安全 Go
一文搞懂Go语言标准库,log
Go 语言的标准库中提供了一个简单的 log 日志包,它不仅提供了很多函数,还定义了一个包含很多方法的类型 Logger。Logger 会打印每条日志信息的日期、时间,默认输出到标准错误。Fatal 系列函数会在写入日志信息后调用 os.Exit(1)。Panic 系列函数会在写入日志信息后 panic。下面详细介绍下标准库log的基本使用。
171 0
一文搞懂Go语言标准库,log
|
存储 Java API
深入理解Kotlin协程suspend工作原理(初学者也能看得懂)
深入理解Kotlin协程suspend工作原理(初学者也能看得懂)
深入理解Kotlin协程suspend工作原理(初学者也能看得懂)
|
编解码 Rust 小程序