当你开始写宏时的一些技巧
When You Get to Be Smart Writing a Macro

原始链接: https://tonsky.me/blog/hashp/

本文详细介绍了一种巧妙的解决方案,将调试工具 `hashp`(`println` 的简洁替代品)与 Clojure 的 thread-first (`->`) 和 thread-last (`->>`) 宏无缝集成。`hashp` 简单地打印一个值及其原始形式。 最初,直接在线程宏中使用 `hashp` 会导致语法错误,原因是 reader tag 扩展顺序的问题。作者曾考虑为每种线程风格创建单独的宏,但目标是找到一个统一的解决方案。 突破点在于利用一个“探测器”——一个接受两个参数的匿名函数,其中一个参数是特殊的 `::undef` 符号。通过观察 `::undef` 在宏展开期间的位置,代码可以确定它是否在 `->` 或 `->>` 宏内部。一个多重arity函数处理*不在*线程宏外部的情况,保持表达式不变。 这种优雅的方法允许 `hashp` 在 thread-first 和 thread-last 宏内都能正确工作,并且只使用一个实现,从而增强了它作为强大调试工具的效用,并且作为 Clojure+ 的一部分提供。

## Clojure 宏讨论 一则 Hacker News 讨论围绕编程中宏的适当使用,尤其是在 Clojure 环境下。最初的帖子以一个看似简单的宏 (#p) 为例,引发了争论。 许多评论者认为,宏应该仅用于函数无法实现的功能,例如线程宏和新的定义形式。他们强调一个层级关系:“数据 > 函数 > 宏”,提倡尽可能使用函数,因为宏本身具有复杂性并可能存在隐藏行为。 一个主要担忧是宏可能无法访问函数体,从而产生限制(例如在 `core.async` 中)。 还有人指出宏的脆弱性以及调试被其转换的代码的难度。 虽然有些人承认宏在代码高尔夫或特定优化方面的优势,但共识倾向于除非绝对必要,否则应避免使用它们,而应优先考虑清晰度和可维护性。 讨论还涉及可以减轻使用括号带来的一些不便的编辑器工具(Paredit, Parinfer)。
相关文章

原文

Day-to-day programming isn’t always exciting. Most of the code we write is pretty straightforward: open a file, apply a function, commit a transaction, send JSON. Finding a problem that can be solved not the hard way, but smart way, is quite rare. I’m really happy I found this one.

I’ve been using hashp for debugging for a long time. Think of it as a better println. Instead of writing

(println "x" x)

you write

#p x

It returns the original value, is shorter to write, and doesn’t add an extra level of parentheses. All good. It even prints original form, so you know which value came from where.

Under the hood, it’s basically:

(defn hashp [form]
  `(let [res# ~form]
     (println '~form res#)
     res#))

Nothing mind-blowing. It behaves like a macro but is substituted through a reader tag, so defn instead of defmacro.

Okay. Now for the fun stuff. What happens if I add it to a thread-first macro? Nothing good:

user=> (-> 1 inc inc #p (* 10) inc inc)
Syntax error macroexpanding clojure.core/let at (REPL:1:1).
(inc (inc 1)) - failed: vector? at: [:bindings] spec: :clojure.core.specs.alpha/bindings

Makes sense. Reader tags are expanded first, so it replaced inc with (let [...] ...) and then tried to do threading. Wouldn’t fly.

We can invent a macro that would work, though:

(defn p->-impl [first-arg form fn & args]
  (let [res (apply fn first-arg args)]
    (println "#p->" form "=>" res)
    res))

(defn p-> [form]
  (list* 'p->-impl (list 'quote form) form))

(set! *data-readers* (assoc *data-readers* 'p-> #'p->))

Then it will expand to

user=> '(-> 1 inc inc #p-> (* 10) inc inc)

(-> 1
  inc
  inc
  (p->-impl '(* 10) * 10)
  inc
  inc)

and, ultimately, work:

user=> (-> 1 inc inc #p-> (* 10) inc inc)
#p-> (* 10) => 30
32

Problem? It’s a different macro. We’ll need another one for ->>, too, so three in total. Can we make just one instead?

Turns out you can!

Trick is to use a probe. We produce an anonymous function with two arguments. Then we call it in place with one argument (::undef) and see where other argument goes.

Inside, we check where ::undef lands: first position means we’re inside ->>, otherwise, ->:

((fn [x y]
   (cond
     (= ::undef x) <thread-last>
     (= ::undef y) <thread-first>))
 ::undef)

Let’s see how it behaves:

(macroexpand-1
  '(-> "input"
     ((fn [x y]
        (cond
          (= ::undef x) <thread-last>
          (= ::undef y) <thread-first>))
      ::undef)))

((fn [x y]
   (cond
     (= ::undef x) <thread-last>
     (= ::undef y) <thread-first>))
   "input" ::undef)

(macroexpand-1
  '(->> "input"
     ((fn [x y]
        (cond
          (= ::undef x) <thread-last>
          (= ::undef y) <thread-first>))
      ::undef)))

((fn [x y]
   (cond
     (= ::undef x) <thread-last>
     (= ::undef y) <thread-first>))
   ::undef "input")

If we’re not inside any thread first/last macro, then no substitution will happen and our function will just be called with a single ::undef argument. We handle this by providing an additional arity:

((fn
   ([_]
    <normal>)
   ([x y]
    (cond
      (= ::undef x) <thread-last>
      (= ::undef y) <thread-first>)))
   ::undef)

And boom:

user=> #p (- 10)
#p (- 10)
-10

user=> (-> 1 inc inc #p (- 10) inc inc)
#p (- 10)
-7

user=> (->> 1 inc inc #p (- 10) inc inc)
#p (- 10)
7

#p was already very good. Now it’s unstoppable.

You can get it as part of Clojure+.

联系我们 contact @ memedata.com