在Clojure中共享命名空间之间的功能

时间:2013-03-22 22:23:49

标签: function clojure namespaces abstraction

我可能会以错误的方式接近这一点,所以请原谅我的天真:

为了学习Clojure,我已经开始将我的OAuth客户端库移植到Clojure。我这样做是通过包装clj-http来实现的,就像我在Python库中包装Python Requests一样。到目前为止,这似乎工作得很好,我真的很高兴看到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.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抽象?

3 个答案:

答案 0 :(得分:5)

考虑一下Zach Tellman的图书馆Potemkin。 Zach将其描述为“重组命名空间和代码结构的函数集合”。

Potemkin并非没有争议。 Here's Clojure邮件列表中一个帖子的开头,Stuart Sierra明确表示他并不喜欢这个想法。

答案 1 :(得分:2)

我将回答我的问题,虽然感谢所有评论的人:安德鲁的回答非常有用,虽然它没有完全回答这个问题,但肯定能找到答案。我认为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函数。

修改

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

答案 2 :(得分: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