October 26, 2019
By: Kevin

Macro第一部分-入门

  1. Macro介绍
  2. 一个例子
  3. 递归

Macro介绍

Lisp语言的一个特性是code is data,Macro是这一特性的终极体现,它允许程序员简洁方便的扩展自己的语言,是一种meta-programming的技巧。

hand

Macro一般会用于:

  1. 语言扩展 (本文会展示,让clojure支持infix notation)
  2. 更简洁的代码 (第二部分我们看下如何使用Macro简化AntdD的调用)
  3. 封装重复模式 (第三部分我们会分析Reitit的Route和Ring的App)

Clojure很多的核心表达式都是Macro,比如when

(defmacro when ;defmacro 定义一个叫做"when"的Macro
  "Evaluates test. If logical true, evaluates body in an implicit do.";; Doc 字符串
  {:added "1.0"}     ;; Meta Info
  [test & body]      ;; 第一个参数用来判断,后面的一组(一个或者多个表达式)
  (list 'if test (cons 'do body))) ;; 返回一个list,这个list的实质是一个if表达式,把参数中body的表达式装在do里

使用上Macro和函数没有区别

(when (= 1 1 )  (println "1还真等于1呢!"))

macroexpand-1可以让我们看到when这个Macro做的工作: 展开为一个if语句,自动加上do

(macroexpand-1 '(when (= 1 1 )  (println "1还真等于1呢!")))

Macro是一类函数,以代码为输入,以代码为输出。而且只在编译阶段执行。

Lisp/clojure的函数可以在任何时候执行,即 1. 运行时 2. 编译时 3. 读入时执行。Macro就是一个在编译期间执行的函数,理解这一点对掌握宏尤其重要。

Macro的参数,在Macro执行前不会进行求值(eval),这一点也有别于函数,函数的参数如果是一个表达式,会首先求值再进入函数执行流程,而Macro的作为参数的表达式结构会原封不动传给Macro,供进一步操作。

Macro返回的结果会立即进行eval。读Macro的代码,发现多数情况是返回的是一个表达式(也有返回字符串,关键词,数字的情况,比较少)。但实际执行Macro返回的是是对这个list的求值(eval the-result-list)

Hello world

;; 在lumo环境中(本blog),namespace下的宏需要放到$macros中
(ns cljs.user$macros)

;; 最基础的宏
;; 注意返回值是个list

(defmacro hello [x]
  (list str "hello " x) )

执行一下

(cljs.user/hello "world!")

macroexpand用来展开宏,返回一个未经过eval的原始结果,是一个非常常用的宏调试方法。

(macroexpand '(cljs.user/hello "world!"))

macroexpand的结果可以再次进行eval, 和直接执行这个宏的结果是一样的。

(eval (macroexpand-1 '(cljs.user/hello "world!")))

一个例子

lisp中list的第一个元素会被当成函数,这种被称为prefix notion,如果我们希望支持infix notation,我们可以写一个宏:

;; form是个表达式 (12 + 12)
(defmacro infix [form]
  (list (second form) (first form) (nth form 2)))

看下结果:

(cljs.user/infix (15 + 20))

Macro是生成代码的一种方式,我们可以看看它具体是怎么加工、处理的代码

(macroexpand-1 '(cljs.user/infix (15 + 20)))

Macro的复杂性在于理解它的执行场景是两段式的(普通宏是这样的,如果宏生成宏的话,可以再增加一层。。。)

  1. 编译期间执行生成代码,此阶段要防止下阶段代码提前执行(引起来)
  2. 生成的代码要在执行期间结合执行环境(外部变量),要能避免冲突,比如说你生成的代码里有变量a,外部环境中也有个a,会造成冲突

这两段执行塞在同一段代码中,区分的方式就是引用/反引用,

改进版

(defmacro infix-better [form]
  `(~(second form) ;注意 syntax-quote (`)  unquote (~) !
    ~(first form)
    ~(nth form 2)))

上面这个例子中使用了syntax-quoteunquote

当然,当前的版本没有考虑表达式嵌套层级的问题比如说(15 + (10 + 10))这种情况就应对不了,所以以下执行会出错

(cljs.user/infix (15 + (10 + 10)))

我们以递归的方式实现一版:

(defmacro r-infix [form]
  (cond (not (seq? form))
    form

    (= 1 (count form))
    `(r-infix ~(first form))

    :else
    (let [operator (second form)
          first-arg (first form)
          others (nth form 2)]
      `(~operator
         (r-infix ~first-arg)
         (r-infix ~others)))))

展开:

(macroexpand-1 '(cljs.user/r-infix (15 + (10 + 10))))

求值:

(cljs.user/r-infix (15 + (10 + 10)))

但是对于多个并列表达式的情况,还是有问题的

(cljs.user/r-infix (10 + (2 * 3) + (4 * 5)))

递归

怎么算懂了递归:懂递归的时候

(defmacro r-infix [form]
  (cond (not (seq? form))
    form

    (= 1 (count form))
    `(r-infix ~(first form))

    :else
    (let [operator (second form)
          first-arg (first form)
          others (rest (rest form) )]
      `(~operator
         (r-infix ~first-arg)
         (r-infix ~others)))))

大功告成

(cljs.user/r-infix (10 + (2 * 3) + (4 * 5)))
Tags: clojurescript macro