2013-03-22 6 views
7

ich das sehr gut in der falschen Art und Weise nähern kann, so bitte ich meine Naivität vergeben:-Sharing-Funktionen zwischen Namespaces in Clojure

Um Clojure zu lernen, die ich für Python meine OAuth-Client-Bibliothek begonnen habe Portierung auf Clojure. Ich mache das, indem ich clj-http auf die gleiche Weise umschlinge, wie ich Python-Anfragen in die Python-Bibliothek einpacke. Das scheint bisher ziemlich gut zu funktionieren und ich genieße es wirklich, die Umsetzung in Clojure lebendig werden zu sehen.

Ich bin jedoch auf ein Problem gestoßen: Ich habe vor, sowohl OAuth 1.0 und 2.0 zu unterstützen, als auch die entsprechenden Funktionen in zwei Dateien zu teilen: oauth1.clj und oauth2.clj. Nun sollte jede Datei idealerweise eine Reihe von Funktionen verfügbar machen, die HTTP-Verben entsprechen.

(ns accord.oauth2) 

... 

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

Diese Funktionen werden im Wesentlichen identisch sein und in der Tat sind jetzt völlig identisch zwischen oauth1.clj und oauth2.clj. Meine erste Reaktion war, diese Funktionen in core.clj zu verschieben und sie dann in den entsprechenden OAuth-Namespaces (oauth1, oauth2) zu verlangen, um zu vermeiden, dass derselbe Code zweimal geschrieben wird.

Das ist in Ordnung, solange ich die verwiesenen Funktionen in der Datei verwende, d. H. Oauth1.clj oder oauth2.clj. Aber lassen Sie uns sagen, dass wir diese Bibliothek verwenden möchten, wie ich (hier in der REPL alternativ Ihr Programm) bin zu wollen, so etwas wie dieses:

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

... 

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

Der var oauth2/get nicht gefunden wird, weil es in den Namensraum in oauth2 ziehen .clj allein scheint es nicht zu entlarven, als wäre es tatsächlich in diesem Namensraum. Ich möchte sie nicht mit mehr Funktionen umhüllen, weil das im Grunde den Zweck verfehlt; Die Funktionen sind so einfach (sie wickeln nur eine request Funktion) Ich würde sie an drei Stellen schreiben, im Wesentlichen, wenn ich das tun würde.

Ich bin sicher, ich bin nicht Nekromanten in Clojure richtig grokking und darüber hinaus vielleicht die allgemeine Art des Denkens über Abstraktion Probleme und Code-Sharing idiomatisch.

Also ich frage mich, was die idiomatische Lösung dafür ist? Gehe ich das völlig falsch?

Edit:

Hier ist eine Vereinfachung des Problems: https://gist.github.com/maxcountryman/5228259

Beachten Sie, dass das das Ziel ist, die HTTP-Verb funktioniert eine Zeit zu schreiben. Sie brauchen keine speziellen Versandarten oder so etwas. Sie sind schon in Ordnung, wie sie sind. Das Problem ist, dass sie nicht von accord.oauth1 oder accord.oauth2 ausgesetzt sind, d. H. Wenn Ihr Programm zum Beispiel accord.oauth2 erfordert.

Wenn diese Python waren, konnten wir importieren nur die Funktionen wie folgt aus: from accord.core import get, post, put, ... in accord.oauth1 und accord.oauth2 und dann, wenn wir das accord.oauth1 Modul verwendet würden wir Zugriff auf alle diese importierten Funktionen haben, zum Beispiel import accord.oauth2 as oauth2 ... oauth2.get(...).

Wie können wir das in Clojure tun oder wie sollten wir idiomatisch für diese Art von DRY Abstraktion sorgen?

+0

"Meine erste Reaktion war, diese Funktionen in core.clj zu verschieben und sie dann in den entsprechenden OAuth-Namespaces (oauth1, oauth2) zu verlangen, um zu vermeiden, denselben Code zweimal zu schreiben." Hast du das gemacht? Wenn ja, warum müssen Sie es im zweiten Codeblock anfordern? In welcher Datei/welchem ​​Namespace befindet sich dieser zweite Codeblock? –

+0

Bitte geben Sie den spezifischen Compilerfehler an. – noahlz

+0

@tieTY Ich habe die HTTP-Verb-Funktionen in core.clj verschoben, was der Namensraum 'accord.core' ist. Dann benötigen die Namensräume 'accord.oauth1' und' accord.oauth2' diese Funktionen, indem sie: alle aus 'accord.core' in ihre jeweiligen Namespaces verweisen. Das Problem dabei ist, dass Sie 'accord.oauth2' nicht benötigen können, sagen Sie in der REPL oder von Ihrem Programm, und verwenden Sie dann die HTTP-Funktionen auf diese Weise:' accord.oauth2/get'. Wenn die Funktionen tatsächlich zweimal in jeder Datei geschrieben würden, würde das funktionieren. Ich versuche das jedoch zu vermeiden. :) – maxcountryman

Antwort

1

Ich werde meine Frage beantworten, obwohl ich mich bei allen bedanke, die kommentiert haben: Andrews Antwort ist sehr informativ und obwohl sie die Frage nicht ganz beantwortet, führt sie doch zu Antworten. Ich denke Potemkin wird dies tun, aber ich ging weiter und schrieb meine eigene Lösung basierend auf this thread. Ich werde sagen, dass ich diesen Ansatz im Allgemeinen nicht als idiomatisch empfinde, basierend auf einigen Antworten hier und weiteren Diskussionen im IRC, mag dies jedoch für Anwendungen mit begrenzter Nutzung, wie meiner, sinnvoll sein.

Aber die Frage zu beantworten, sollte diese Funktion tun, was ich hatte ursprünglich beabsichtigt:

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

Dann könnte man es so etwas wie diese aufrufe, lassen Sie sie sagen, dass wir dies in foo.clj setzen (wenn Sie das sehen Kern ich in der edit hinzugefügt):

(ns testing.foo) 

(immigrate `testing.baz) 

Nun, wenn wir benötigen testing.foo in der REPL:

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

Nachdem ich mit Stuart Sierra im IRC gesprochen und den Andrew-Link email thread gelesen hatte, kam ich zu dem Schluss, dass dies nicht unbedingt der beabsichtigte Weg ist, Namespaces zu verwenden.

Statt einen besseren Weg, meine Bibliothek implementieren könnte wie folgt aussehen:

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

jedoch gegeben, dass ich die Endbenutzer mit der saubersten API wie möglich präsentieren wollen, kann ich gehen Sie vor und nutzen die immigrate Funktion in diesem sehr begrenzten Bereich des "Importierens" der HTTP-Methodenfunktionen.

Edit:

Nach einem weiteren Diskussion, ich glaube, die obige Lösung sollte im allgemeinen nicht verwendet werden, wie ich schon sagte. Und für meinen Anwendungsfall werde ich wahrscheinlich mit meiner letzten Lösung gehen, d. H. Mit zwei separaten Namespaces.

0

Eine Option zum Entwerfen der Lösung wäre die Verwendung von Multimethoden mit Standardimplementierung.

;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 
+0

Der Parameter 'serv' unterscheidet die Funktionen. Es gibt keine Notwendigkeit, sie zweimal zu schreiben. – maxcountryman

4

Betrachten wir einen Blick auf Zach Tellman Bibliothek Potemkin haben. Zach beschreibt es als "eine Sammlung von Funktionen zum Reorganisieren der Struktur von Namespaces und Code".

Potemkin ist nicht ohne Kontroverse. Here's der Beginn eines Threads auf der Clojure Mailingliste, wo Stuart Sierra klar ist, dass er kein Fan der Idee ist.

+0

Danke, dieser Thread, den Sie verlinkt haben, ist ziemlich informativ. Es scheint allgemein der Konsens zu bestehen, dass man den Python-Weg nicht (eher) nachahmen kann. Stattdessen sollten Sie Namensräume flach halten. Also denke ich, dass das Beste für mich hier ist, dass der Benutzer zwei Namespaces benötigt: 'accord.oauth2' und' accord.core', was wie folgt funktionieren könnte: '(require '[accord.oauth2: as oauth2]) 'und' (erfordern '[accord.core: as client]) '' then '(def serv (oauth2/service ...))' und '(client/get serv ...)'. – maxcountryman