Реализация пользовательских структур данных с использованием Clojure Протоколы

Возможно, я пропустил весь вопрос о протоколах, но мой вопрос: могут ли протоколы использоваться, чтобы определять, как итерации пользовательской структуры данных или как println будет печатать объект?

Предполагая отображение с двумя векторами,

{:a [] :b []}

Когда вы вызываете его первым, я хотел бы взять из: a vector, но когда conj на этой структуре я хотел бы соединить с: b. Могу ли я использовать протоколы для достижения такого типа поведения?

Ответ 1

Некоторые вещи по-прежнему реализуются как интерфейсы Java в Clojure; из них, я бы сказал, некоторые из них, вероятно, останутся навсегда, чтобы облегчить сотрудничество с кодом Clojure с других языков JVM.

К счастью, при определении типа с помощью deftype вы можете использовать новый тип любых Java-интерфейсов, которые вам нужны (о чем упоминал Брайан в комментарии выше), а также о любых методах java.lang.Object. Пример, соответствующий вашему описанию, может выглядеть так:

(deftype Foo [a b]
  clojure.lang.IPersistentCollection
  (seq [self] (if (seq a) self nil))
  (cons [self o] (Foo. a (conj b o)))
  (empty [self] (Foo. [] []))
  (equiv
   [self o]
   (if (instance? Foo o)
     (and (= a (.a o))
          (= b (.b o)))
     false))
  clojure.lang.ISeq
  (first [self] (first a))
  (next [self] (next a))
  (more [self] (rest a))
  Object
  (toString [self] (str "Foo of a: " a ", b: " b)))

Пример того, что вы можете сделать с ним в REPL:

user> (.toString (conj (conj (Foo. [] []) 1) 2))
"Foo of a: [], b: [1 2]"
user> (.toString (conj (conj (Foo. [:a :b] [0]) 1) 2))
"Foo of a: [:a :b], b: [0 1 2]"
user> (first (conj (conj (Foo. [:a :b] [0]) 1) 2))
:a
user> (Foo. [1 2 3] [:a :b :c])
(1 2 3)

Обратите внимание, что REPL печатает его как seq; Я считаю, что из-за встроенной реализации clojure.lang.ISeq. Вы можете пропустить его и заменить метод seq одним возвращающим (seq a) для распечатанного представления с использованием пользовательского toString. str всегда использует toString.

Если вам требуется индивидуальное поведение функций семейства pr (включая println и т.д.), вам нужно будет изучить пользовательский print-method для вашего типа. print-method - многомерный метод, определенный в clojure.core; посмотрите core_print.clj в источниках Clojure, например, реализации.

Ответ 2

Я играл с пользовательскими коллекциями и хотел настроить вывод на REPL, поэтому я закончил следовать рекомендациям Michal в последнем пункте. Я включил фрагмент кода о том, как это сделать, потому что я нашел, что просеивание через источник заняло у меня некоторое время, поскольку я не нашел это объясненным где-либо еще.

(defmethod print-method your.custom.collection.goes.Here [c, ^java.io.Writer w]
    (.write w (str "here is my custom output: " c)))

Это удобно, например, в случаях, когда seq всегда печатает ваш пользовательский вектор с круглыми скобками (как в примере с Михаллом), и вы хотите, чтобы квадратные скобки, такие как обычные векторы Clojure:

(defmethod print-method your.custom.Vector [v, ^java.io.Writer w]
    (.write w (str (into [] v))))

Это также означает, что теперь можно реализовать seq, чтобы фактически вернуть последовательность вашего типа данных, а не просто реализовать его для вывода REPL.