Implementing Custom Data Structures Using Clojure Protocols
Asked Answered
K

2

10

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?

Kitchen answered 31/5, 2010 at 14:18 Comment(3)
Not yet since nearly no low-level fn (except reduce) have been "protocolized" but you can use defrecord or deftype to define a datatype that behaves like you wish.Secund
Yes but then I have to implement the functions that act on it also, creating redundant functions names, such as head which works as first for my data structure, no?Kitchen
If you use deftype, you can provide implementations for the various interfaces that Clojure uses (like clojure.lang.ISeq) that do what you want.Hosmer
A
13

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.

Antispasmodic answered 31/5, 2010 at 18:51 Comment(0)
L
0

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.

Lading answered 11/5, 2014 at 11:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.