Clojure函数自引用无法获取调用实例元数据的技术问询
我太懂你遇到的这个坑了——给函数加了元数据后,在函数内部通过自引用就是拿不到这些元数据,核心原因其实是Clojure里的元数据是不可变的:with-meta根本不会修改原函数对象,而是返回一个包裹原函数的全新实例。你在函数内部引用的I,是定义时的那个原始函数对象,不是后来带元数据的f,自然拿不到:rr 5。
先拆解下你的示例代码:
(let [I (fn I [x] (println I) (println (meta I)))] (let [f (with-meta I {:rr 5})] (println I) (println f) (f I)))
当你调用f I时,函数内部跑的是原始I的逻辑,这里的I指向的是最初定义的fn,不是带元数据的f,所以(meta I)返回的是原函数的默认元数据(一般是nil),完全不是你附加的:rr 5。
下面给你几种实用的解决方案,你可以根据场景选:
方案1:通过参数传递当前函数实例
最简单的思路就是调用时把带元数据的实例本身传进去,让函数内部通过参数访问:
(let [I (fn [self x] (println self) (println (meta self)))] (let [f (with-meta #(I %1 %2) {:rr 5})] (println I) (println f) (f f I)))
这里用#(I %1 %2)包装原始函数,调用f时把f自己作为第一个参数传入,函数内部的self就是带元数据的实例,(meta self)就能正确获取:rr 5了。
方案2:用原子引用捕获当前实例
如果你不想改调用方式,可以用原子来存带元数据的函数实例,让函数内部通过原子引用访问:
(let [self-ref (atom nil) I (fn [x] (println @self-ref) (println (meta @self-ref))) f (with-meta I {:rr 5})] (reset! self-ref f) (println I) (println f) (f I))
先定义一个原子self-ref,把带元数据的f存进去后,函数内部引用@self-ref就能拿到当前被调用的实例了。这种方式不用改函数的调用签名,但引入了可变状态,适合单线程场景用。
方案3:用reify实现IFn接口(最优雅的方式)
Clojure的函数本质是实现了clojure.lang.IFn接口的对象,我们可以用reify手动实现这个接口,这样在invoke方法里就能用this直接引用当前实例:
(let [I (reify clojure.lang.IFn (invoke [this x] (println this) (println (meta this)))) f (with-meta I {:rr 5})] (println I) (println f) (f I))
这种方式不需要额外参数或可变状态,this直接指向当前被调用的带元数据实例,运行后(meta this)会输出{:rr 5},完美解决你的问题。
总结一下:Clojure的函数不可变性决定了with-meta会生成新实例,要让内部访问到当前实例,要么通过参数传递,要么用状态捕获,要么直接实现IFn接口获取this引用。
内容的提问来源于stack exchange,提问作者xstreamer

