Совместное использование функций между пространствами имен в Clojure

Я вполне могу подходить к этому неправильно, поэтому, пожалуйста, простите меня за мою наивность:

Чтобы изучить Clojure, я начал переносить свою клиентскую библиотеку OAuth для Python на Clojure. Я делаю это, обертывая clj-http так же, как обертываю запросы Python в библиотеке Python. Похоже, что до сих пор это работает довольно хорошо, и мне действительно нравится видеть, как реализация в 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.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?


person maxcountryman    schedule 22.03.2013    source источник
comment
Моей первой реакцией было переместить эти функции в core.clj, а затем потребовать их в соответствующих пространствах имен OAuth (oauth1, oauth2), чтобы не писать один и тот же код дважды. Ты сделал это? Если да, то зачем требовать его во втором блоке кода? В каком файле / пространстве имен находится этот второй блок кода?   -  person Daniel Kaplan    schedule 23.03.2013
comment
Укажите конкретную ошибку компилятора.   -  person noahlz    schedule 23.03.2013
comment
@tieTYT Я переместил функции, связанные с HTTP-глаголами, в core.clj, который является пространством имен accord.core. Затем пространства имен accord.oauth1 и accord.oauth2 требуют этих функций путем ссылки: all from accord.core на их соответствующие пространства имен. Проблема в том, что вы не можете затем потребовать accord.oauth2, скажем, в REPL или из вашей программы, а затем использовать функции HTTP таким образом: accord.oauth2/get. Если бы функции действительно были написаны дважды в каждом файле, это сработало бы. Однако я пытаюсь этого избежать. :)   -  person maxcountryman    schedule 23.03.2013
comment
@noahz => (oauth2/get foo "http://example.com/") CompilerException java.lang.RuntimeException: No such var: oauth2/get, compiling:(NO_SOURCE_PATH:1)   -  person maxcountryman    schedule 23.03.2013
comment
Вы тестировали только свою библиотеку из REPL? Возможно, вы могли бы заменить текст своего вопроса более наглядным кодом. Попробуйте написать собственное приложение или модульные тесты.   -  person noahlz    schedule 23.03.2013
comment
У меня есть две идеи. 1) Реорганизуйте свой код, чтобы иметь третье пространство имен, что-то вроде accord.oauth-common, и импортируйте его, чтобы получить общие функции, или 2) просто используйте def для повторного связывания функций, которые вы хотите в каждом пространстве имен (вместо того, чтобы полностью повторно объявлять их) , например (def get oauth1/get). Я лично выбрал бы первый вариант.   -  person DaoWen    schedule 23.03.2013
comment
@DaoWen под импортом вы имеете в виду require или что-то еще?   -  person maxcountryman    schedule 23.03.2013
comment
Вот упрощенный пример того, что мы хотели бы сделать: gist.github.com/maxcountryman/5228259   -  person maxcountryman    schedule 23.03.2013
comment
@maxcountryman - Да, require или use.   -  person DaoWen    schedule 23.03.2013
comment
@DaoWen, похоже, не работает. Та же проблема: переменная недоступна извне файла, где этот файл требуется в программе. См., Например, мою суть. По сути, это то, что я делал с accord.core, где accord.core был третьим пространством имен. (Это сработало бы, если бы я использовал def, как вы указываете во втором варианте, но я бы не стал этого делать.)   -  person maxcountryman    schedule 23.03.2013
comment
@maxcountryman - В контексте вашей сути я предлагал вам просто (ns testing.bar (:use [test core baz])) в bar.clj.   -  person DaoWen    schedule 23.03.2013
comment
@maxcountryman: Нет, если вы используете третье пространство имен (например, ядро), клиентский код должен использовать пространство имен это вместо конкретного для версии. Использование (или требование) делает переменные доступными только в пределах тех ns, где они использовались.   -  person Goran Jovic    schedule 23.03.2013
comment
@DaoWen, который не будет работать вне этого файла. Итак, из REPL: (require '[testing.bar :as bar]) ... (bar/qux "test") не дает такой var: bar / qux. Однако в bar.clj: (def qux baz/qux) будет работать. Видишь, что я пытаюсь сделать?   -  person maxcountryman    schedule 23.03.2013
comment
@maxcountryman - Я хочу сказать, что вы всегда должны требовать / использовать оба пространства имен, если вам нужна их функциональность. Любое найденное вами решение, которое действительно объединяет пространства имен так, как вы хотите, чтобы они сочетались, просто будет выполнять мое другое предложение, переопределив все привязки под капотом, чтобы они указывали на то же, что и привязки другого пространства имен. Я по-прежнему считаю, что лучшим решением будет просто (:use [test core baz]), поскольку это всего лишь несколько дополнительных символов.   -  person DaoWen    schedule 24.03.2013


Ответы (3)


Взгляните на библиотеку Зака ​​Теллмана Potemkin. Зак описывает его как «набор функций для реорганизации структуры пространств имен и кода».

Потемкин не лишен противоречий. Вот начало ветки в списке рассылки Clojure, где Стюарт Сьерра Понятно, что он не фанат идеи.

person Andrew    schedule 23.03.2013
comment
Спасибо, эта ветка, которую вы связали, довольно информативна. Похоже, что все согласны с тем, что вы не можете (скорее, не должны) эмулировать путь Python. Вместо этого вы должны держать пространства имен плоскими. Поэтому я думаю, что лучше всего для меня это просто потребовать от пользователя двух пространств имен: accord.oauth2 и accord.core, которые могут работать следующим образом: (require '[accord.oauth2 :as oauth2]) и (require '[accord.core :as client]), затем (def serv (oauth2/service ...)) и (client/get serv ...). - person maxcountryman; 23.03.2013

Я собираюсь ответить на свой вопрос, хотя спасибо всем, кто прокомментировал: ответ Эндрю очень информативен, и хотя он не совсем отвечает на вопрос, он, безусловно, приводит к ответам. Я действительно думаю, что Потемкин сделает это, но я пошел дальше и написал свое собственное решение на основе эту ветку. Я скажу, что я не считаю этот подход в целом идиоматическим, основываясь на некоторых ответах здесь и дальнейшем обсуждении в 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)

Теперь, если нам потребуется test.foo в REPL:

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

Поговорив со Стюартом Сьеррой по IRC и прочитав переписку по электронной почте, Эндрю связал, Я пришел к выводу, что это не обязательно предполагаемый способ использования пространств имен.

Вместо этого лучший способ реализовать мою библиотеку может выглядеть так:

=> (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, я могу пойти дальше и использовать функцию immigrate в этой очень ограниченной области «импорта» функций метода HTTP.

Изменить:

После дальнейшего обсуждения я думаю, что вышеупомянутое решение не следует использовать, как я уже сказал. И для моего варианта использования я, вероятно, выберу свое последнее решение, то есть использую два отдельных пространства имен.

person maxcountryman    schedule 25.03.2013

Один из вариантов разработки решения - использовать мультиметоды с предоставленной реализацией по умолчанию.

;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 
person Ankur    schedule 23.03.2013
comment
Параметр serv будет различать функции. Нет необходимости писать их дважды. - person maxcountryman; 23.03.2013