Удивительное поведение, связанное с записями, протоколами и компиляцией

Я столкнулся со мной, немного удивительным поведением, казалось бы, связанных с clojure записями.

Настройка выполняется следующим образом:

  • Одно пространство имен определяет тип записи:

    (ns defrecordissue.arecord)
    
    (defrecord ARecord [])
    
  • Другое пространство имен определяет протокол и расширяет его до записи тип, определенный в 1:

    (ns defrecordissue.aprotocol
      (:require [defrecordissue.arecord])
      (:import [defrecordissue.arecord ARecord]))
    
    (defprotocol AProtocol
      (afn [this]))
    
    (extend-protocol AProtocol
      ARecord
      (afn [this] 42))
    
  • Третье пространство имен создает экземпляр записи и вызывает функция протокола в записи.

    (ns defrecordissue.aot1
      (:require [defrecordissue.aprotocol]
                [defrecordissue.arecord]))
    
    (defrecordissue.aprotocol/afn (defrecordissue.arecord/->ARecord))
    

Когда пространство имен defrecordissue.aot1 скомпилировано, в моем случае с использованием lein compile defrecordissue.aot1, компиляция не выполняется с следующее исключение:

Exception in thread "main" java.lang.IllegalArgumentException: No implementation of method: :afn of protocol: #'defrecordissue.aprotocol/AProtocol found for class: defrecordissue.arecord.ARecord, compiling:(aot1.clj:5:1)
    at clojure.lang.Compiler$InvokeExpr.eval(Compiler.java:3463)
    at clojure.lang.Compiler.compile1(Compiler.java:7153)
    at clojure.lang.Compiler.compile(Compiler.java:7219)
    at clojure.lang.RT.compile(RT.java:398)
    at clojure.lang.RT.load(RT.java:438)
    at clojure.lang.RT.load(RT.java:411)
    at clojure.core$load$fn__5018.invoke(core.clj:5530)
    at clojure.core$load.doInvoke(core.clj:5529)
    at clojure.lang.RestFn.invoke(RestFn.java:408)
    at clojure.core$load_one.invoke(core.clj:5336)
    at clojure.core$compile$fn__5023.invoke(core.clj:5541)
    at clojure.core$compile.invoke(core.clj:5540)
    at user$eval7.invoke(NO_SOURCE_FILE:1)
    at clojure.lang.Compiler.eval(Compiler.java:6619)
    at clojure.lang.Compiler.eval(Compiler.java:6609)
    at clojure.lang.Compiler.eval(Compiler.java:6582)
    at clojure.core$eval.invoke(core.clj:2852)
    at clojure.main$eval_opt.invoke(main.clj:308)
    at clojure.main$initialize.invoke(main.clj:327)
    at clojure.main$null_opt.invoke(main.clj:362)
    at clojure.main$main.doInvoke(main.clj:440)
    at clojure.lang.RestFn.invoke(RestFn.java:421)
    at clojure.lang.Var.invoke(Var.java:419)
    at clojure.lang.AFn.applyToHelper(AFn.java:163)
    at clojure.lang.Var.applyTo(Var.java:532)
    at clojure.main.main(main.java:37)
Caused by: java.lang.IllegalArgumentException: No implementation of method: :afn of protocol: #'defrecordissue.aprotocol/AProtocol found for class: defrecordissue.arecord.ARecord
    at clojure.core$_cache_protocol_fn.invoke(core_deftype.clj:541)
    at defrecordissue.aprotocol$fn__40$G__35__45.invoke(aprotocol.clj:5)
    at clojure.lang.AFn.applyToHelper(AFn.java:161)
    at clojure.lang.AFn.applyTo(AFn.java:151)
    at clojure.lang.Compiler$InvokeExpr.eval(Compiler.java:3458)
    ... 25 more 

Если я изменю 3), чтобы построить класс записи напрямую, например:

(ns defrecordissue.aot2
  (:require [defrecordissue.aprotocol]
            [defrecordissue.arecord]))

(defrecordissue.aprotocol/afn (defrecordissue.arecord.ARecord.))

Успешная компиляция.

Мое подозрение в том, что это как-то связано с http://dev.clojure.org/jira/browse/CLJ-371, но я не понимаю точно, что происходит.

Я также должен добавить, что без lein clean компиляция завершается успешно во второй раз, поскольку класс для записи теперь доступен на CLASSPATH. Поэтому я могу обойти эту проблему с помощью AOT-компиляции пространство имен, определяющее тип записи.

Я создал простой проект leiningen на GitHub, который иллюстрирует вопрос, см. README для использования: https://github.com/ragnard/defrecordissue

Почему я вижу это поведение и как правильно его избежать?

UPDATE

Я добавил новую ветку в репозиторий GitHub, лучше иллюстрирующую основную проблему: https://github.com/ragnard/defrecordissue/tree/more-realistic/

Проблема возникает независимо от того, где (т.е. в каком пространстве имен) запись экземпляр построен.

Ответ 1

Я могу воспроизвести проблему с вашим репо. Здесь три решения, которые работают для меня:

  • Сообщите lein compile, чтобы скомпилировать другие пространства имен:

    lein compile defrecordissue.aprotocol defrecordissue.arecord defrecordissue.aot1
    
  • Помещенный

    :aot [defrecordissue.aprotocol defrecordissue.arecord defrecordissue.aot1]
    

    в project.clj.

  • Помещенный

    :aot :all
    

    в project.clj.

Последние два делают lein compile выполнение работы lein aot1 (в случае 2.) и оба lein aot1 и lein aot2 (в случае 3.).

Ответ 2

Я все время сталкиваюсь с этим. Это то, что я придумал, и это работает:

(defmacro with-datatype
  [datatype & body]
  (let [last-dot (.lastIndexOf ^String (str datatype) ".")
        ns (-> datatype
               str
               (subs 0 last-dot)
               symbol)]
     `(do
        (require (quote ~ns))
        (import ~datatype)
        [email protected])))

(defmacro extend-type*
  [datatype & extensions]
  `(with-datatype ~datatype
     (extend-type ~datatype
       [email protected])))

(defmacro extend-protocol*
  [protocol & specs]
  (let [splitter (let [l (atom nil)]
                   #(if (symbol? %) (reset! l %) @l))
        specs (partition-by splitter specs)]
    `(do
       [email protected](for [[datatype & extensions] specs]
           `(extend-type* ~datatype ~protocol [email protected])))))

Затем вам нужно только изменить свой второй блок кода на:

(ns defrecordissue.aprotocol)

(defprotocol AProtocol
  (afn [this]))

(extend-protocol* AProtocol
  defrecordissue.arecord.ARecord
  (afn [this] 42))

Возможно, это не самое чистое решение, но вам больше не нужно использовать AOT для своих библиотек.