Clojure 中的命名空间之间共享函数
Posted
技术标签:
【中文标题】Clojure 中的命名空间之间共享函数【英文标题】:Sharing functions between namespaces in Clojure 【发布时间】:2013-03-12 22:15:24 【问题描述】:我很可能以错误的方式处理这个问题,所以请原谅我的幼稚:
为了学习 Clojure,我开始将我的 OAuth 客户端库用于 Python 移植到 Clojure。我通过包装 clj-http 来做到这一点,就像我在 Python 库中包装 Python 请求一样。到目前为止,这似乎运行良好,我真的很高兴看到实现在 Clojure 中实现。
但是我遇到了一个问题:我计划同时支持 OAuth 1.0 和 2.0,并将各自的功能拆分为两个文件:oauth1.clj 和 oauth2.clj。现在,理想情况下,每个文件都应该公开一组与 HTTP 动词对应的函数。
(ns accord.oauth2)
...
(defn get
[serv uri & [req]]
((:request serv) serv (merge req :method :get :url uri)))
这些函数本质上是相同的,实际上现在在 oauth1.clj 和 oauth2.clj 之间是完全相同的。我的第一反应是将这些函数移到 core.clj 中,然后在各自的 OAuth 命名空间(oauth1、oauth2)中要求它们,以避免重复编写相同的代码。
只要我在文件中使用引用的函数,即 oauth1.clj 或 oauth2.clj,这很好。但是假设我们想按照我的意图使用这个库(在 REPL 中,或者在你的程序中),如下所示:
=> (require '[accord.oauth2 :as oauth2]) ;; require the library's oauth2 namespace
...
=> (oauth2/get my-service "http://example.com/endpoint") ;; use the HTTP functions
找不到 var oauth2/get
,因为仅将其拉入 oauth2.clj 中的命名空间似乎并没有像它实际上在该命名空间中一样暴露它。我不想用更多的功能来包装它们,因为这基本上违背了目的;这些函数非常简单(它们只是包装了一个 request
函数),基本上,如果我要这样做的话,我会在三个地方编写它们。
我确定我在 Clojure 中没有正确地理解命名空间,而且可能是惯用地思考抽象问题和代码共享的一般方式。
所以我想知道解决这个问题的惯用方法是什么?我是不是完全走错了路?
编辑:
这是问题的简化:https://gist.github.com/maxcountryman/5228259
请注意,目标是一次性编写 HTTP 动词函数。他们不需要特殊的调度类型或类似的东西。他们已经很好了。问题是它们不会从accord.oauth1
或accord.oauth2
中暴露出来,例如,当您的程序需要accord.oauth2
时。
如果这是 Python,我们可以将如下函数导入:from accord.core import get, post, put, ...
到 accord.oauth1
和 accord.oauth2
,然后当我们使用 accord.oauth1
模块时,我们将可以访问所有这些导入的函数,例如import accord.oauth2 as oauth2
... oauth2.get(...)
.
我们如何在 Clojure 中做到这一点,或者我们应该如何惯用地提供这种 DRY 抽象?
【问题讨论】:
“我的第一反应是将这些函数移动到 core.clj 中,然后在各自的 OAuth 命名空间(oauth1、oauth2)中要求它们,以避免重复编写相同的代码。”你做了这个了吗?如果你这样做了,为什么你需要在第二个代码块中使用它?第二个代码块在哪个文件/命名空间中? 请提供具体的编译错误。 @tieTYT 我将与 HTTP 动词相关的函数移到 core.clj 中,即accord.core
命名空间。然后 accord.oauth1
和 accord.oauth2
命名空间需要这些函数,方法是将 accord.core
中的 :all 引用到它们各自的命名空间中。这样做的问题是,您不能在 REPL 或您的程序中要求 accord.oauth2
,然后以这种方式使用 HTTP 函数:accord.oauth2/get
。如果在每个文件中实际编写了两次函数,那将起作用。然而,我试图避免这种情况。 :)
我有两个想法。 1) 重构您的代码以拥有第三个命名空间,例如 accord.oauth-common
,然后导入它以获取常用函数,或者 2) 只需使用 def
在每个命名空间中重新绑定您想要的函数(而不是完全重新绑定) - 声明它们),例如(def get oauth1/get)
。我个人会选择第一个选项。
@maxcountryman - 就你的要点而言,我的建议是你应该只在bar.clj
中使用(ns testing.bar (:use [test core baz]))
。
【参考方案1】:
设计解决方案的一个选项是使用提供默认实现的多方法。
;The multi methods which dispatch on type param
(defmulti get (fn [serv uri & [req]] serv))
(defmulti post (fn [serv uri & [req]] serv))
;get default implementation for any type if the type doesn't provide its own implementation
(defmethod get :default [serv uri & [req]]
"This is general get")
;post doesn't have default implementation and provided specific implementation.
(defmethod post :oauth1 [serv uri & [req]]
"This is post for oauth1")
(defmethod post :oauth2 [serv uri & [req]]
"This is post for oauth2")
;Usage
(get :oauth1 uri req) ;will call the default implementation
(get :oauth2 uri req) ;will call the default implementation
(post :oauth1 uri req) ;specific implementation call
(post :oauth2 uri req) ;specific call
【讨论】:
serv
参数将区分功能。没有必要写两次。【参考方案2】:
考虑看看 Zach Tellman 的图书馆 Potemkin。 Zach 将其描述为“用于重组命名空间和代码结构的函数集合”。
波将金并非没有争议。 Here's Clojure 邮件列表上的一个线程的开头,Stuart Sierra 明确表示他不是这个想法的粉丝。
【讨论】:
谢谢,你链接的这个主题信息量很大。似乎普遍的共识是你不能(而不应该)模仿 Python 的方式。相反,您应该保持命名空间平坦。所以我认为对我来说最好的办法就是让用户需要两个命名空间:accord.oauth2
和 accord.core
,它们可以像这样工作:(require '[accord.oauth2 :as oauth2])
和 (require '[accord.core :as client])
然后是 (def serv (oauth2/service ...))
和 @987654328 @.【参考方案3】:
我将回答我的问题,但感谢所有评论的人:Andrew 的回答内容丰富,虽然它不能完全回答问题,但它确实会带来答案。我确实认为 Potemkin 会这样做,但我继续并根据this thread 编写了自己的解决方案。我会说,根据此处的一些回复和 IRC 中的进一步讨论,我不觉得这种方法通常是惯用的,但它可能对有限的用例有意义,例如我的。
但是要回答这个问题,这个函数应该做我原本打算的:
(defn immigrate
[from-ns]
(require from-ns)
(doseq [[sym v] (ns-publics (find-ns from-ns))]
(let [target (if (bound? v)
(intern *ns* sym (var-get v))
(intern *ns* sym))]
(->>
(select-keys (meta target) [:name :ns])
(merge (meta v))
(with-meta '~target)))))
然后你可以像这样调用它,假设我们把它放在 foo.clj 中(如果你看到我在编辑中添加的要点):
(ns testing.foo)
(immigrate `testing.baz)
现在如果我们在 REPL 中需要 testing.foo:
=> (require '[testing.foo :as foo])
=> (foo/qux "hi!")
;; "hi!"
在 IRC 上与 Stuart Sierra 交谈并阅读了email thread Andrew 链接后,我得出结论,这不一定是使用命名空间的预期方式。
实现我的库的更好方法可能如下所示:
=> (require '[accord.oauth2 :as oauth2])
=> (def my-serv (oauth2/service 123 456 ...))
=> (require '[accord.http :as http])
=> (http/get my-serv "http://example.com/endpoint")
但是,鉴于我想为最终用户提供尽可能干净的 API,我可能会继续在这个非常有限的“导入”HTTP 方法函数的范围内使用 immigrate
函数。
编辑:
经过进一步讨论,我认为上述解决方案一般不应该使用,正如我已经说过的。对于我的用例,我可能会使用我的最后一个解决方案,即使用两个单独的命名空间。
【讨论】:
以上是关于Clojure 中的命名空间之间共享函数的主要内容,如果未能解决你的问题,请参考以下文章