【问题标题】:Writing a log macro in Clojure在 Clojure 中编写日志宏
【发布时间】:2016-12-09 14:13:09
【问题描述】:

我正在尝试this teaching text on Clojure 中的问题 4 并且对我的程序的结果感到困惑:

(def logging-enabled true)
(defmacro log
  "uses a var, logging-enabled, to determine whether or not to print an expression to the console at compile time. If logging-enabled is false, (log :hi) should macroexpand to nil. If logging-enabled is true, (log :hi) should macroexpand to (prn :hi)."
  [expr]
  (if logging-enabled
    `(prn ~expr)
    nil
    ))

当我尝试测试我的程序时,评估下面的每个表单

(let [logging-enabled true]
  (log "hi there"))

(let [logging-enabled false]
  (log "hi there"))

(let [logging-enabled true]
  (macroexpand (log "hi there")))

导致 REPL 回复完全相同的响应:

user=> 
"hi there"
nil
user=>

hi there 表示宏返回了 true 分支 它的if 表格。 nil 将是封闭的返回值 let 表单。

但问题是:为什么要打印 "hi there" 在我的第二个测试表单中,logging-enabled 的范围为 false 上面?

还有:为什么macroexpand 不扩展我的宏而只是做 和上面的两个测试表一样吗?

【问题讨论】:

    标签: clojure macros


    【解决方案1】:

    宏在编译时扩展,所以当你的宏运行时logging-enabled 指的是 var logging-enabled,而不是由封闭的 let 绑定的 logging-enabled。您需要在返回的表单中包含if 条件并引用它以防止名称被解析:

    (defmacro log
      [expr]
      `(if ~'logging-enabled
         (prn ~expr)))
    

    【讨论】:

    • 我对@9​​87654326@ 有点困惑……这与~logging-enabled 有何不同?
    • 另请注意:macroexpand 似乎甚至没有扩展您的新宏 - 它似乎在评估它。
    • @TerrenceBrannon - 你怎么打电话给macroexpand(macroexpand '(log "message")) => (if logging-enabled (clojure.core/prn "message")) 给我。您需要引用 logging-enabled 以防止它被解析为 your-ns/logging-enabled var,因为您想动态绑定它。
    • 你不应该在这里使用~'logging-enabled。它将扩展为不合格的符号,但您确实希望它扩展为合格的符号。
    • @Lee - 我没有控制台来查看历史记录,但基于这些 cmets,我相信我忘记了我使用的表达式前面的 quote
    【解决方案2】:

    让我们看一下问题4的文字(强调添加):

    编写一个宏log,它使用一个变量logging-enabled,以确定是否在编译时将表达式打印到控制台。如果logging-enabled 为假,(log :hi) 应该宏扩展为nil。如果logging-enabled 为真,则(log :hi) 应宏扩展为(prn :hi)。为什么要在编译期间而不是在运行程序时进行此检查?你可能会失去什么?

    您的宏符合此规范。我们可以通过以下实验来证实这一点:

    ;; With logging-enabled false
    (def logging-enabled false)
    
    (macroexpand '(log :hi))
    ;;=> nil
    
    ;; With logging-enabled true
    (def logging-enabled true)
    
    (macroexpand '(log :hi))
    ;;=> (clojure.core/prn :hi)
    

    注意我们传递给macroexpand'(log :hi) 的内容。 'quote 的阅读器快捷方式。所以这相当于(quote (log :hi)):

    (read-string "'(log :hi)")
    ;;=> (quote (log :hi))
    

    这很重要,因为macroexpand 是一个函数,所以在调用macroexpand 之前计算它的参数。

    你可能会失去什么?

    我认为这是在编译时做太多事情的一个很好的例子。通过在编译时执行检查,您可以通过在运行时不执行检查来节省一点时间。 但是,您将失去动态打开和关闭登录的能力——无论 logging-enabled 设置为什么,当特定的 log 表单为 macroexpanded 时,该表单是否会打印消息或不是。

    如果在运行时执行此检查会更实用。然后我们可以在运行时启用和禁用日志记录。

    在我们这样做的同时,我们不妨将logging-enabled 设为dynamic var。这样,我们可以策略性地启用/禁用特定动态范围内的日志记录。

    (def ^:dynamic *logging-enabled* false)
    

    按照惯例,动态变量的名称以*s 结尾。这不是必需的,但它确实提醒我们 var 是动态的。

    现在,我们可以将log 定义为:

    (defmacro log [expr]
      `(when *logging-enabled*
         (prn ~expr)))
    

    ` 表示quasi-/syntax-quotation。基本上,它类似于quote,除了您可以使用~ 转义引号。

    使用log 的这个定义,评估log 表单——查找*logging-enabled* 的(动态)值并检查结果值会产生少量运行时开销。但是,如果*logging-enabled*false,就是这样——我们不会评估传递给log 的表达式,也不会向控制台打印任何内容。所以运行时开销很小。

    我们可以像这样使用这个新版本的log

    ;; Root value for *logging-enabled* is false
    (log :hi)
    ;;=> nil
    
    ;; dynamically bind *logging-enabled* to true
    (binding [*logging-enabled* true]
      (log :hi))
    ;; Prints ":hi"
    

    【讨论】:

    • 我会修改“除非你可以使用 ~ 来转义引号。”还提到您也可以使用~@ 转义引用。
    猜你喜欢
    • 1970-01-01
    • 2010-11-14
    • 1970-01-01
    • 2018-05-23
    • 2020-05-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-06-29
    相关资源
    最近更新 更多