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

max*_*man 9 abstraction namespaces function clojure

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

为了学习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})))
Run Code Online (Sandbox Code Playgroud)

这些函数基本相同,实际上现在在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
Run Code Online (Sandbox Code Playgroud)

oauth2/get找不到var,因为单独将它拉入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.oauth1accord.oauth2,然后当我们使用accord.oauth1,我们将有机会获得所有这些进口的功能,例如模块import accord.oauth2 as oauth2... oauth2.get(...).

我们怎么能在Clojure中做到这一点,或者我们应该如何习惯性地提供这种DRY抽象?

And*_*rew 5

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

波将金并非没有争议.这是 Clojure邮件列表中一个帖子的开头,Stuart Sierra明确表示他不是这个想法的粉丝.


max*_*man 2

我将回答我的问题,但感谢所有发表评论的人:安德鲁的回答非常丰富,虽然它没有完全回答问题,但它确实会带来答案。我确实认为波将金会这样做,但我继续并基于此线程编写了我自己的解决方案。我想说的是,根据此处的一些回复和 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)))))
Run Code Online (Sandbox Code Playgroud)

然后你可以像这样调用它,假设我们把它放在 foo.clj 中(如果你看到我在编辑中添加的要点):

(ns testing.foo)

(immigrate `testing.baz)
Run Code Online (Sandbox Code Playgroud)

现在,如果我们在 REPL 中需要testing.foo:

=> (require '[testing.foo :as foo])
=> (foo/qux "hi!")
;; "hi!"
Run Code Online (Sandbox Code Playgroud)

在 IRC 上与 Stuart Sierra 交谈并阅读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")
Run Code Online (Sandbox Code Playgroud)

然而,考虑到我想向最终用户提供尽可能干净的 API,我可能会继续immigrate在“导入”HTTP 方法函数的非常有限的范围内使用该函数。

编辑:

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