clojure实战——宏

本文探讨了Clojure中的宏基础知识,强调宏在编译期间的特性及其潜在风险。介绍了如何创建简单宏、宏调试工具以及宏安全问题,建议在必要时谨慎使用宏,主要用于特殊语法构造和提前计算。

clojure实战——宏

本博客主要介绍clojure中宏相关的基础知识,因为自己没能很深入研究clojure的宏,所以做不到深入的讲解。但根据自己及什么clojure用的比较好的人的经验,能不用宏就不用宏,用宏、特别是逻辑复杂的宏机会真的很少,因此我个人觉得本博客所涉及的东西已经足够应付一般的场景了。

clojure宏概述

clojure宏在编译期间被求值,而不是文本替换(和C语言的预编译不同),宏的求值过程也叫做”宏展开”;

宏求值的结果是返回一个clojure的数据结构,这个数据结构会代替宏原来的位置。

clojure的源代码会被clojure reader读入,将其以文本形式求值出一个clojure数据结构,如:(fn [a] :a 123)求值出来的数据结构是一个列表:包含一个符号,一个包含符号的vector,一个关键字和一个数字;而这个数据结构本身就是clojure语言的基本数据结构。同样,宏(reverse-it (nf [a] :a 123))也会返回一个同样的数据结构。

一门语言的代码可以用语言自身的数据结构来描述,称为”同像性”

一个简单的宏

一个翻转符号(symbol)的宏(必须将clojure的符号反着写,如prn必须写成nrp)

(defmacro reverse-it
  [form]
  (walk/postwalk #(if (symbol? %)
                    (symbol (str/reverse (name %)))
                    %)
                 form))

(comment
  (reverse-it (nrp "lz"))
  ;; => "lz" clojure编译期间会将其求值为:(prn "lz")

  (reverse-it (prn "lz"))
  ;; => CompilerException java.lang.RuntimeException: Unable to resolve symbol: nrp in this context
  )

宏的调试

clojure类库中提供了一些工具函数,用于宏的调试(查看宏扩展出来的代码),最常用的有:

clojure.core/macroexpand-1
查看宏产生的代码,扩展宏一次,如果宏里面调用其他宏,或扩展之后返回的还是宏调用,则其他宏不会被扩展。

clojure.core/macroexpand
如果扩展完一次宏之后,返回的还是一个宏调用,则会再次扩展,直到顶级形式不再是个宏。注意这不是嵌套的宏!

clojure.walk/macroexpand-all
彻底扩展一个宏,包括所有的嵌套宏。但它对一些特殊情况处理不完全正确,不赘述,一般用不到。

示例:

(comment
  (macroexpand-1 '(reverse-it (nrp "lz")))
  ;; => (prn "lz")

  (macroexpand '(reverse-it (nrp "lz")))

  (walk/macroexpand-all '(reverse-it (nrp "lz"))))

宏安全

宏是在编译期执行的,而在编译期间,宏并不知道某个符号是不是已经被定义。它看到的就是列表、符号以及其他数据结构。它返回的也是列表、符号和其他数据结构。所以在宏里面用了一些外部没有定义的符号,编译时也不会出错,但是执行的时候就会出问题。

示例:

(defmacro unknow-symbol
    [form]
    `(str "unknow symbol " a ~form))
;; a没有定义时,执行出错:
(unknow-symbol "bbb")
;; => CompilerException java.lang.RuntimeException: No such var: clojure-study.micro/a,

;; a定义时,执行正常
(def a "a")
(unknow-symbol "bbb")
;; => "unknow symbol abbb"

宏的主要风险是:宏产生的代码与外部代码发生不正常的交互!
宏无法访问运行时的值,不能作为值进行组合或者传递。
对于那些对于需要传递高阶函数的地方,避免用宏。

如:

(comment
  (map reverse-it '((+ 1 3) (+ 3 4)))
  ;; => CompilerException java.lang.RuntimeException: Can't take value of a macro: #'clojure-async.micro-symbols/reverse-it,
  ;; 编译时出错,宏不能作为一个值传递给map,
  ;; 虽然map的第一个参数是一个函数fn,但是clojure中函数也是数据,也是一个值。
  (map #(reverse-it %) [(+ 1 3) (+ 3 4)])
  ;; => NullPointerException
  ;; 运行时错误
)

宏的另一个危险的地方是:当红内部要绑定一个本地符号时,这个符号可能会和外部代码的冲突,这样宏一旦扩展出来,极有可能就发生异常,而这种异常通过查阅代码是很难发觉的。

为了避免上述情况,可以在宏内绑定一个本地绑定时,符号以#结尾。

在语法引述形式中任何以#结尾的符号都会被自动扩展,并且对于前缀相同的符号,会被扩展为同一个符号的名字。这样可以避免宏里面的符号与外部代码的相冲突。

如:

(comment
  `(x# x#)
  ;; => (x__2284__auto__ x__2284__auto__)

  (defmacro println-mcro
    [y]
    (let [y# "macro"]
      `(println ~y# ~y)))

  (println-mcro 1)
  ;; => macro 1

  )

小结

  • 应该尽量少用宏,只有在函数满足不了的情况下,才用宏。
  • 即使要用宏,应该只是用它做一些简单的组织工作,真正的逻辑都要放在真正的函数中。
  • 宏的使用场景:
    • 需要特殊的求职语义;
    • 需要自定义语法——特别是一些领域特定表示法。
    • 需要在编译器提前计算一些中间值。
  • 用之前,始终问问自己,用函数不能解决吗?!
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值