Implementing Custom Data Structures Using Clojure

2020-06-03 03:16发布

问题:

I may have missed the whole point about protocols but my question is, can protocols be used to dictate how to iterate a custom data structure or how println would print the object?

Assuming a map with two vectors,

{:a [] :b []}

When called first on it I would like to take from the :a vector but when conj on this structure i would like to conj to :b. Can I use protocols to achieve this type of behavior?

回答1:

Some things are still implemented as Java interfaces in Clojure; of those, I'd say some are likely to stay that way forever to ease cooperating with Clojure code from other JVM languages.

Fortunately, when defining a type using deftype, you can have the new type implement any Java interfaces you require (which Brian mentioned in a comment above), as well as any methods of java.lang.Object. An example to match your description might look like this:

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

A sample of what you can do with it at the 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)

Note that the REPL prints it as a seq; I believe that's because of the inline implementation of clojure.lang.ISeq. You could skip it and replace the seq method with one returning (seq a) for a printed representation using the custom toString. str always uses toString, though.

If you need custom behaviour of pr family functions (including println etc.), you'll have to look into implementing a custom print-method for your type. print-method is a multimethod defined in clojure.core; have a look at core_print.clj in Clojure's sources for example implementations.



回答2:

I was playing with custom collections and wanted to customise the output to the REPL so I ended up following Michal's advice on the last point. I have included the code snippet as to how to do this because I found sifting through the source took me a while as I didn't find this explained anywhere else.

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

This is handy, for example, in cases where the seq is always printing your custom vector with parentheses (as with Michal's example) and you want square brackets like regular Clojure vectors:

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

It also means the way can now implement seq to actually return a sequence of your data type rather than just having to implement it for REPL output.



标签: clojure