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.oauth1和accord.oauth2,然后当我们使用accord.oauth1,我们将有机会获得所有这些进口的功能,例如模块import accord.oauth2 as oauth2... oauth2.get(...).
我们怎么能在Clojure中做到这一点,或者我们应该如何习惯性地提供这种DRY抽象?
我将回答我的问题,但感谢所有发表评论的人:安德鲁的回答非常丰富,虽然它没有完全回答问题,但它确实会带来答案。我确实认为波将金会这样做,但我继续并基于此线程编写了我自己的解决方案。我想说的是,根据此处的一些回复和 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 方法函数的非常有限的范围内使用该函数。
编辑:
经过进一步讨论,我认为上述解决方案一般不应该使用,正如我已经说过的。对于我的用例,我可能会采用最后一个解决方案,即使用两个单独的名称空间。