关于反射:您能否在Clojure中获得已加载函数的“代码作为数据”?

Can you get the “code as data” of a loaded function in Clojure?

换句话说,"好吧,代码就是数据……"

该线程解决了如何从源文件读取的问题,但是我想知道如何将已经加载的函数的s表达式转换为可以读取和操作的数据结构。

换句话说,如果我说

1
(defn example [a b] (+ a b))

我无法在运行时获取该列表? 这不是"代码即数据"的重点吗?

这确实是一个一般的Lisp问题,但我正在Clojure中寻找答案。


您可以使用clojure.repl/source宏来获取符号的来源:

1
2
3
4
5
6
7
8
9
10
11
user> (source max)
(defn max
 "Returns the greatest of the nums."
  {:added"1.0"
   :inline-arities >1?
   :inline (nary-inline 'max)}
  ([x] x)
  ([x y] (. clojure.lang.Numbers (max x y)))
  ([x y & more]
   (reduce1 max (max x y) more)))
nil

但这只是答案的一部分。 AFAICT source查找定义给定符号的源文件名和行号,然后从文件中打印源代码。因此,source将不适用于您没有源的符号,即AOT编译的Clojure代码。

回到最初的问题,您可以将source看作是读取与给定符号关联的元数据,然后简单地打印出来。即这是作弊。这绝不是将"代码作为数据"返回给您的方式,其中代码是指经过编译的clojure函数。

在我看来,"代码即数据"是指lisps的功能,其中源代码实际上是lisp数据结构,因此可以由lisp阅读器读取。也就是说,我可以创建一个有效的lisp代码的数据结构,然后eval进行创建。

例如:

1
2
user=> (eval '(+ 1 1))
2

这里的'(+ 1 1)是一个文字列表,它由clojure阅读器读取,然后被评估为clojure代码。

更新:Yehonathan Sharvit在评论之一中询问是否可以修改功能代码。以下代码片段从函数中读取源代码,修改结果数据结构,最后评估数据结构,从而定义了新函数my-nth

1
2
3
4
(eval
 (let [src (read-string (str (source-fn 'clojure.core/nth)"\
"))]
   `(~(first src) my-nth ~@(nnext src))))

syntax-quote行以defn形式将nth替换为my-nth


您可以使用source函数获取最新版本的clojure的源。

1
2
3
4
5
6
7
8
9
10
11
12
user=> (source nth)
(defn nth
 "Returns the value at the index. get returns nil if index out of
  bounds, nth throws an exception unless not-found is supplied.  nth
  also works for strings, Java arrays, regex Matchers and Lists, and,
  in O(n) time, for sequences."
  {:inline (fn  [c i & nf] `(. clojure.lang.RT (nth ~c ~i ~@nf)))
   :inline-arities #{2 3}
   :added"1.0"}
  ([coll index] (. clojure.lang.RT (nth coll index)))
  ([coll index not-found] (. clojure.lang.RT (nth coll index not-found))))
nil

要获取字符串作为值,可以将其包装在with-out-str中:

1
2
3
4
5
6
7
8
9
10
11
12
13
user=> (with-out-str (source nth))
"(defn nth\
  \"Returns the value at the index. get returns nil if index out of\
  bounds, nth throws an exception unless not-found is supplied.  nth\
  also works for strings, Java arrays, regex Matchers and Lists, and,\
  in O(n) time, for sequences.\"\
  {:inline (fn  [c i & nf] `(. clojure.lang.RT (nth ~c ~i ~@nf)))\
   :inline-arities #{2 3}\
   :added \"1.0\"}\
  ([coll index] (. clojure.lang.RT (nth coll index)))\
  ([coll index not-found] (. clojure.lang.RT (nth coll index not-found))))\
"
user=>


那是我的信息;见到您很高兴;-)顺便说一句,该主题中给出的参考答案非常好阅读;因此,如果您有兴趣,不妨花些时间阅读它们。回到您的问题,尽管source似乎适用于通过文件加载的代码,但并非在所有情况下都有效。我认为,特别是,它对于repl中定义的功能不起作用。

1
2
3
4
5
6
7
8
9
10
user=> (def foo (fn [] (+ 2 2)))
#'user/foo
user=> (source foo)
Source not found
nil
user=> (defn foo2 [] (+ 2 2))
#'user/foo2
user=> (source foo2)
Source not found
nil

挖一点...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
user=> (source source)
(defmacro source
 "Prints the source code for the given symbol, if it can find it.
  This requires that the symbol resolve to a Var defined in a
  namespace for which the .clj is in the classpath.

  Example: (source filter)"
  [n]
  `(println (or (source-fn '~n) (str"Source not found"))))
nil
user=> (source clojure.repl/source-fn)
(defn source-fn
 "Returns a string of the source code for the given symbol, if it can
  find it.  This requires that the symbol resolve to a Var defined in
  a namespace for which the .clj is in the classpath.  Returns nil if
  it can't find the source.  For most REPL usage, 'source' is more
  convenient.

  Example: (source-fn 'filter)"
  [x]
  (when-let [v (resolve x)]
    (when-let [filepath (:file (meta v))]
      (when-let [strm (.getResourceAsStream (RT/baseLoader) filepath)]
        (with-open [rdr (LineNumberReader. (InputStreamReader. strm))]
          (dotimes [_ (dec (:line (meta v)))] (.readLine rdr))
          (let [text (StringBuilder.)
                pbr (proxy [PushbackReader] [rdr]
                      (read [] (let [i (proxy-super read)]
                                 (.append text (char i))
                                 i)))]
            (read (PushbackReader. pbr))
            (str text)))))))
nil

是的,看起来它试图从类路径中加载源文件以尝试为您吐出来。使用Clojure时,我了解到的一件事是,十分之九的内容是有用的。