【问题标题】:Creating a Ruby-like `binding.pry` macro in Clojure在 Clojure 中创建一个类似 Ruby 的 `binding.pry` 宏
【发布时间】:2022-01-02 03:00:06
【问题描述】:

我一直在努力创建一个宏,它允许我将&env 中的任何内容动态绑定到binding 表单中,然后委托给pry 之类的函数来打开一个可以看到的REPL那些绑定&env 符号。

我的简单pry func,按预期工作

(defn pry []
  (print (str "pry(" *ns* ")> "))
  (flush)
  (let [expr (read)]
    (when-not (= expr :exit)
      (println (eval expr))
      (recur))))

使用pry 函数:

clojure-noob.core=> (def a 1)
#'clojure-noob.core/a
clojure-noob.core=> (pry)
pry(clojure-noob.core)> (+ a 1)
2
pry(clojure-noob.core)> :exit
nil
clojure-noob.core=>

我尝试创建binding 的动态调用:

(defmacro binding-pry []
  (let [ks (keys &env)]
    `(let [ks# '~ks
           vs# [~@ks]
           bs# (vec (interleave ks# vs#))]
       (binding bs# (pry)))))

但是,这会失败,因为内部符号 bs# 没有扩展为实际向量,而是生成的符号,binding 抛出了 clojure.core/binding requires a vector for its binding 异常。

clojure-noob.core=> (let [a 1 b 2] (binding-pry))
Syntax error macroexpanding clojure.core/binding at (/tmp/form-init14332359378145135257.clj:1:16).
clojure.core/binding requires a vector for its binding in clojure-noob.core:

clojure-noob.core=> 

带有调试打印的代码引用表单,打印时解析bs#符号,但我不知道在构造binding表单时如何使其解析为向量。

(defmacro binding-pry []
  (let [ks (keys &env)]
    `(let [ks# '~ks
           vs# [~@ks]
           bs# (vec (interleave ks# vs#))]
       (println bs#)
       `(binding bs# (pry)))))

clojure-noob.core=> (let [a 1 b 2] (binding-pry))
[a 1 b 2]
(clojure.core/binding clojure-noob.core/bs__2464__auto__ (clojure-noob.core/pry))
clojure-noob.core=>

我非常有信心我没有正确地解决这个问题,但我没有看到其他方法。

【问题讨论】:

  • 这可能是我见过的新用户帖子中最好的第一个问题。
  • 您要求“类似 Ruby”的行为,但这是一个 Clojure 问题,并不是每个人都对 Ruby 足够熟悉以知道您期望什么行为。您有一个“简单化”pry 函数的示例,但正如您所说,它按预期工作。但是,我没有看到您对binding-pry 的期望的示例。如果您添加预期的行为,它会更容易提供帮助。

标签: clojure


【解决方案1】:

The Joy of Clojure 演示了一个 break 宏,它已经做到了这一点。我不能在这里复制它的源代码,因为它是 EPL 而不是 CC。但你可以在https://github.com/joyofclojure/book-source/blob/b76ef15/first-edition/src/joy/breakpoint.clj 看到它的来源。它还引用了contextual-eval 函数:https://github.com/joyofclojure/book-source/blob/b76ef15/first-edition/src/joy/macros.clj#L4-L7

【讨论】:

  • 这个contextual-eval(以及依赖于它的break)的一个问题是它假设环境中绑定的值“可以嵌入代码中”,这是不正确的对于各种对象。例如,如果环境中有Delay,它将不起作用。 eval-in-context 的正确实现可以在 plumatic/plumbing 库中找到 eval-bound
【解决方案2】:

改进尝试的第一步可能是写作:

(defmacro binding-pry []
  (let [ks (keys &env)]
    `(binding [~@(interleave ks ks)] (pry))))

这仍然不起作用,因为binding 期望符号可以解析为现有的动态变量。为了解决这个问题,你可以让binding-pry 引入如下所示的变量:

(defmacro binding-pry []
  (let [ks (keys &env)]
    `(do
       ~@(map (fn [k] `(def ~(with-meta k {:dynamic true}))) ks)
       (binding [~@(interleave ks ks)] (pry)))))

但这可能会产生不良的副作用,例如使用新的变量名污染命名空间或使现有变量动态化。所以我更喜欢 amalloy 的回答中提到的方法,但更好地实现 eval-in-context(请参阅我的评论)。

要根据您的pry 函数编写一个独立的答案,我们首先定义eval-in,它在环境中评估表单:

(defn eval-in [env form]
  (apply
    (eval `(fn* [~@(keys env)] ~form))
    (vals env)))

然后让我们修改pry 以将环境作为参数并使用eval-in 代替eval:

(defn pry [env]
  (let [prompt (str "pry(" *ns* ")> ")]
    (loop []
      (print prompt)
      (flush)
      (let [expr (read)]
        (when-not (= expr :exit)
          (println (eval-in env expr))
          (recur))))))

一个等价的、不那么原始的版本可能是:

(defn pry [env]
  (->> (repeatedly (let [prompt (str "pry(" *ns* ")> ")]
                     #(do (print prompt) (flush) (read))))
       (take-while (partial not= :exit))
       (run! (comp println (partial eval-in env)))))

现在我们可以定义binding-pry如下:

(defmacro binding-pry []
  `(pry ~(into {}
           (map (juxt (partial list 'quote) identity))
           (keys &env))))

最后,这是binding-pry 的直接/“意大利面条”实现:

(defmacro binding-pry []
  (let [ks (keys &env)]
    `(->> (repeatedly (let* [prompt# (str "pry(" *ns* ")> ")]
                        #(do (print prompt#) (flush) (read))))
          (take-while (partial not= :exit))
          (run! (comp println
                      #((eval `(fn* [~~@(map (partial list 'quote) ks)] ~%))
                        ~@ks))))))

【讨论】:

  • 绝对是对我链接到的功能的改进。不过,您似乎有点迷恋无点体操 - 像(for [k (keys &env)] ['~k ~k]) 这样的东西比无点并列/地图版本要简单得多。
  • @amalloy 你的意思是(for [k (keys &env)] [`'~k ~k])(注意`)。但这引入了一个额外的序列(非常小的缺点),因此我们可以将转换器写为(map (fn [k] [`'~k k]))。是的,我一般都喜欢无点(在适当的基础上,不仅仅是为了它),因为它有助于代数处理,但我承认在 Clojure 中它通常感觉很麻烦,尤其是在“点满”的宏中syntax-quote 成语看起来更清晰。
  • 对。我永远不记得如何在 SO cmets 中编写反引号,这使得对宏的评论变得棘手。我担心在编译时引入额外序列的成本为 0%,这使得对可读性的关注更加合理。
猜你喜欢
  • 2023-01-14
  • 1970-01-01
  • 2019-02-26
  • 1970-01-01
  • 2023-03-29
  • 2013-12-01
  • 2015-07-29
  • 1970-01-01
  • 2011-02-27
相关资源
最近更新 更多