在Clojure中在命名空间之间共享函数

9
我可能用了错误的方法,希望您原谅我的天真:
为了学习Clojure,我已经开始将我的Python OAuth客户端库移植到Clojure。我通过在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
oauth2/get变量未找到,因为仅在oauth2.clj中将其引入命名空间似乎并没有将其公开,就好像它实际上在该命名空间中。我不想用更多的函数包装它们,因为那基本上会打败目的;这些函数非常简单(它们只是包装一个request函数),如果我这样做,我将在三个地方编写它们。

我确定我没有正确理解Clojure中的命名空间,而且也许对于抽象问题和代码共享的通用思考方式存在误解。

所以我想知道这个问题的惯用解决方案是什么?我是完全错误的吗?

编辑:

这里是问题的简化:https://gist.github.com/maxcountryman/5228259

请注意,目标是一次编写HTTP动词函数。它们不需要特殊的分派类型或类似的东西。它们已经很好了。问题在于它们没有从accord.oauth1accord.oauth2公开,即当您的程序需要accord.oauth2时。

如果这是Python,我们可以像这样导入函数:from accord.core import get, post, put, ...accord.oauth1accord.oauth2,然后当我们使用accord.oauth1模块时,我们将访问所有这些导入的函数,例如import accord.oauth2 as oauth2...oauth2.get(...)

在Clojure中我们如何做到这一点或者我们应该以惯用方式提供这种DRY抽象?


@noahz => (oauth2/get foo "http://example.com/") 编译器异常 java.lang.RuntimeException: 没有这个变量:oauth2/get,编译中:(NO_SOURCE_PATH:1) - maxcountryman
你只在REPL中测试了你的库吗?也许你可以用更具有说明性的代码替换你问题中的文本。试着编写一个实际的应用程序或单元测试。 - noahlz
2
我有两个想法。1)重构你的代码,增加一个第三个命名空间,例如 accord.oauth-common,并导入它来获取常见的函数;或者2)只需使用 def 来重新绑定每个命名空间中想要的函数(而不是完全重新声明它们),例如 (def get oauth1/get)。我个人会选择第一种选项。 - DaoWen
1
@maxcountryman - 在你的代码片段中,我的建议是你应该在 bar.clj 中只需写 (ns testing.bar (:use [test core baz])) - DaoWen
@DaoWen 那样做在文件之外是行不通的。在REPL中:(require '[testing.bar :as bar]) ... (bar/qux "test")会显示"No such var: bar/qux"。但是在bar.clj中:(def qux baz/qux)将会行得通。明白我试图做什么吗? - maxcountryman
显示剩余9条评论
3个回答

5

考虑查看Zach Tellman的库Potemkin。Zach将其描述为“重新组织命名空间和代码结构的函数集合”。

Potemkin并非没有争议。这里是Clojure邮件列表上的一个线程,Stuart Sierra明确表示他不喜欢这个想法。


谢谢,你提供的这个线程非常有用。看起来普遍共识是不能(或者说不应该)模仿Python的方式。相反,应该保持命名空间的扁平化。所以我想在这里最好的做法就是让用户需要两个命名空间:accord.oauth2accord.core,可以像这样工作:(require '[accord.oauth2 :as oauth2])(require '[accord.core :as client])然后(def serv (oauth2/service ...))(client/get serv ...) - maxcountryman

2
我将回答我的问题,虽然感谢所有留言的人:安德鲁的答案非常有启发性,虽然它并没有完全回答这个问题,但确实引导了答案。我认为波捷金可以做到这一点,但我已经根据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中(如果你看到我在编辑中添加的gist):
(ns testing.foo)

(immigrate `testing.baz)

现在如果我们在 REPL 中需要测试.foo:
=> (require '[testing.foo :as foo])
=> (foo/qux "hi!")
;; "hi!"

在与 Stuart Sierra 在 IRC 上交谈并阅读 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,我可能会在此非常有限的范围内使用immigrate函数来“导入”HTTP方法函数。

编辑:

经过进一步讨论,我认为上述解决方案通常不应该被使用,如我已经说过的那样。对于我的用例,我可能会采用最后一个解决方案,即使用两个独立的命名空间。


0

设计解决方案的一个选择是使用多方法,并提供默认实现。

;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 参数将区分这些函数。没有必要重复编写它们。 - maxcountryman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接