Sharing functions between namespaces in Clojure

痞子三分冷 提交于 2019-12-05 19:31:03

问题


I may very well be approaching this in the wrong way, so please forgive me of my naiveté:

In order to learn Clojure I've begun porting my OAuth client library for Python to Clojure. I'm doing this by wrapping clj-http much in the same way I wrap Python Requests in the Python library. This seems to be working pretty well so far and I'm really enjoying seeing the implementation come to life in Clojure.

However I have run into a problem: I'm planning to support both OAuth 1.0 and 2.0 and have split the respective functions into two files: oauth1.clj and oauth2.clj. Now, each file should ideally expose a set of functions which correspond to HTTP verbs.

(ns accord.oauth2)

...

(defn get
  [serv uri & [req]]
  ((:request serv) serv (merge req {:method :get :url uri})))

These functions will be essentially identical and in fact right now are completely identical between oauth1.clj and oauth2.clj. My first reaction was to move these functions into core.clj and then require them in the respective OAuth namespaces (oauth1, oauth2) so as to avoid writing the same code twice.

This is fine so long as I use the referred functions in the file, i.e. oauth1.clj or oauth2.clj. But let's say we want to use this library as I'm intending (here in the REPL, alternatively your program), something like this:

=> (require '[accord.oauth2 :as oauth2])  ;; require the library's oauth2 namespace

...

=> (oauth2/get my-service "http://example.com/endpoint")  ;; use the HTTP functions

The var oauth2/get is not found because pulling it into the namespace in oauth2.clj alone doesn't seem to expose it as though it were actually in that namespace. I don't want to wrap them with more functions, because that basically defeats the purpose; the functions are so simple (they just wrap a request function) I would be writing them in three places, essentially, if I were to do that.

I'm sure I'm not grokking namespaces in Clojure properly and moreover maybe the general way of thinking about abstraction problems and code sharing idiomatically.

So I'm wondering what the idiomatic solution to this is? Am I going about this completely the wrong way?

Edit:

Here's a simplification of the problem: https://gist.github.com/maxcountryman/5228259

Note that the the goal is to write the HTTP verb functions one time. They don't need special dispatch types or something like that. They're already fine as they are. The issue is that they aren't exposed from accord.oauth1 or accord.oauth2, i.e. when your program requires accord.oauth2 for instance.

If this were Python we could just import the functions like this: from accord.core import get, post, put, ... into accord.oauth1 and accord.oauth2 and then when we used the accord.oauth1 module we would have access to all those imported functions, e.g. import accord.oauth2 as oauth2 ... oauth2.get(...).

How can we do this in Clojure or how should we idiomatically provide for this kind of DRY abstraction?


回答1:


Consider having a look at Zach Tellman's library Potemkin. Zach describes it as "a collection of functions for reorganizing the structure of namespaces and code".

Potemkin isn't without controversy. Here's the start of a thread on the Clojure mailing list where Stuart Sierra is clear that he's not a fan of the idea.




回答2:


I'm going to answer my question although thank you to everyone who commented: Andrew's answer is very informative and while it doesn't quite answer the question it certainly does lead to answers. I do think Potemkin will do this but I went ahead and wrote my own solution based on this thread. I will say I don't feel this approach is generally idiomatic, based on some responses here and further discussion in IRC, however it may make sense for limited use cases, such as mine.

But to answer the question, this function should do what I had originally intended:

(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)))))

Then you could invoke it something like this, let's say we put this in foo.clj (if you see the gist I added in the edit):

(ns testing.foo)

(immigrate `testing.baz)

Now if we require testing.foo in the REPL:

=> (require '[testing.foo :as foo])
=> (foo/qux "hi!")
;; "hi!"

After speaking with Stuart Sierra on IRC and reading the email thread Andrew linked, I came to the conclusion that this is not necessarily the intended way to use namespaces.

Instead a better way to implement my library might look like this:

=> (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")

However given that I want to present the end-user with the cleanest API as possible, I may go ahead and use the immigrate function in this very limited scope of "importing" the HTTP method functions.

Edit:

After further discussion, I think the above solution should not generally be used, as I said already. And for my use-case I'll probably go with my last solution, i.e. using two separate namespaces.




回答3:


One option to design the solution would be to use multimethods with default implementation provided.

;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 


来源:https://stackoverflow.com/questions/15580807/sharing-functions-between-namespaces-in-clojure

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!