Welcome! Please see the About page for a little more info on how this works.

+1 vote
in Clojure by

Goal: Create a new flavor of vector that is identical to the built-in vector, except when invoked, uses a different function (a variadic function defined by me, but concat is a good stand-in for demonstration purposes) instead of get.

Invoking a built-in vector implies get.

(def A (vector :a :b :c))
(get A 1) ;; => :b
(A 1) ;; => :b

No matter what we create to accomplish this task, the standard vector should behave exactly as before.

The new flavor of vector would behave ever-so-slightly differently. First, some example definitions using a new function, foo-vector.

(def B (foo-vector :a :b :c)) ;; => [:a :b :c]
(def C (foo-vector 'foo 'bar 'baz)) ;; => ['foo 'bar 'baz]
(def D (foo-vector 42 99)) ;; => [42 99]

All the usual functions work as before, because, for the most part, they are Clojure vectors.

(first B) ;; => :a
(get C 1) ;; => 'bar
(count D) ;; => 2

The only difference would appear when a "foo-vector" is invoked: Instead of an implied get, it's an implied concat (or some other variadic function, but concat works well for discussion).

(B C D) ;; => [:a :b :c 'foo 'bar 'baz 42 99]

Bonus points: Regular vectors and "foo-vectors" containing the same values would satisfy 'equals'.

(= (vector 1 2 3) (foo-vector 1 2 3)) ;; => true

In all respects, "foo-vectors" B, C, and D are vectors, with the exception that when appearing as the first element of a list (i.e., in the function position), an alternative function is called (in our example, concat) instead of get.

Question: What does foo-vector look like to create this new flavor of vector?

I do not know where to begin researching this problem other than regurgitating terms like IFn, deftype, extends, etc. I greatly appreciate any help.

1 Answer

+1 vote
by

I'll preface this by saying that you probably should not do this - changing the semantics of data structures like this is confusing to users of your code/data. The best solution is probably to just make a function that does what you want.

That said, Clojure's core APIs generally do not rely on concrete types but rather core interfaces and you can use deftype to create your own implementation that implements those interfaces (presuming you want to do this in Clojure).

This is an oldie but a goldie helper function for this (https://gist.github.com/semperos/3835392), which you can use to get the scaffolding. Vectors are most importantly implementations of clojure.lang.IPersistentVector:

user=> (scaffold clojure.lang.IPersistentVector)

  clojure.lang.IPersistentCollection
    (equiv [this G__177])
    (count [this])
    (empty [this])
  clojure.lang.Counted
    (count [this])
  clojure.lang.Seqable
    (seq [this])
  clojure.lang.IPersistentVector
    (assocN [this G__178 G__179])
    (length [this])
    (cons [this G__180])
    (cons [this G__181])
  clojure.lang.Indexed
    (nth [this G__182])
    (nth [this G__183 G__184])
  clojure.lang.Associative
    (entryAt [this G__185])
    (assoc [this G__186 G__187])
    (containsKey [this G__188])
  clojure.lang.IPersistentStack
    (pop [this])
    (peek [this])
  clojure.lang.Reversible
    (rseq [this])
  clojure.lang.ILookup
    (valAt [this G__189])
    (valAt [this G__190 G__191])

If you create a deftype with that impl, wrapping a concrete vector and deferring all of those methods to the internal vector, then you have effectively created a custom vector wrapper with your own type.

Missing from the scaffold above is the thing that actually makes concrete vectors a function, namely, clojure.lang.IFn:

user=> 
  clojure.lang.IFn
    (applyTo [this G__499])
    (invoke [this])
    (invoke [this G__685])
    (invoke [this G__683 G__684])
    ... elided  remaining arities...
  java.util.concurrent.Callable
    (call [this])
  java.lang.Runnable
    (run [this])

so you'd want to throw those into your deftype and have all the invoke's do whatever you want. Varargs is supported by applyTo and you can call AFn.applyToHelper().

If this seems like a lot of tedious work, yes it is. Another path to go is to instead subclass PersistentVector directly and implement the invoke methods. You could either do this in Java or in Clojure via genclass.

But to reiterate, you probably shouldn't do this at all.

by
edited by
Thanks for the quick response, Alex. I didn't mention what I'm trying to do because I wasn't sure it was relevant to the question I'm asking.

This is not for a public-facing library. I'm noodling around with a little language whose grammar is expressed most elegantly like `(vec1 vec2 ...)`. In fact, that's nearly exactly what it looks like pencil on paper.

As it is now, I can say `(v-apply vec1 vec2 ...)`, but I am hoping that if I could re-assign the IFn of those particular vector-like-things, while leaving all the others the same, I could write expressions with an implicit `v-apply` instead of the default `get`.

But in constructing, modifying, and moving around `vec1`, et. al., it would be most convenient to have all of the usual core libs available. Furthermore, I'd like to completely preserve the behavior of the clojure.lang.IPersistentVector, so I definitely don't want to mess around with the regular vectors by changing their IFn. I only want to change the invoked-as-a-function behavior of the vector-like-things I write in the mini-language, while inheriting all the other behaviors of clojure.lang.IPersistentVector.
ago by
Since it seems that `deftype` is bare-bones, I've been attempting to sub-class PersistentVector with `gen-class`.

In a file 'DangerousVector.clj', I have the following:

(ns DangerousVector
  "A vector class that inherits all of Clojure vector behavior, *except* when in
  the function position, concatenates the first vector with the trailing vector."
  (:gen-class
   :name DangerousVector
   :extends clojure.lang.PersistentVector))


(defn -invoke
  ([& vecs]
   (do
     (println "-invoke called")
     (apply concat vecs))))


My intention is to create a new class called 'DangerousVector' that extends clojure.lang.PersistentVector and over-rides the super-class' `invoke` method with a new one that concatenates the vector in the function position with any trailing vectors.

When I compile at a fresh REPL with

blah> (compile 'DangerousVector)

I see the following files generated in directory 'target/classes/dangerous/'

DangerousVector$fn__7418.class
DangerousVector$_invoke.class
DangerousVector$loading__6812__auto____7416.class
DangerousVector__init.class

I am *barely* able to inspect the contents of these files; approximately half the contents are promising-looking strings, with the other half un-renderable characters.

This invocation appears to do something useful

(. DangerousVector (create (cons 1 (cons 2 '(3))))) ;; => [1 2 3]

but when I query its type

(type (. DangerousVector (create '()))) ;; => clojure.lang.PersistentVector

I don't get the expected 'DangerousVector'.

Furthermore, the invoke function doesn't appear to be over-riden.

;; typical `nth` behavior
(let [v (. DangerousVector (create (cons 1 (cons 2 '(3)))))]
  (v 2))
;; => 3


;; not the expected `concat` behavior
(let [v (. DangerousVector (create (cons 1 (cons 2 '(3)))))]
  (v [:a :b :c]))
;; => Unhandled java.lang.IllegalArgumentException
;;    Key must be integer


(Based on this 17-year old chart https://github.com/Chouser/clojure-classes/blob/master/graph-w-legend.svg I focused on clojure.lang.PersistentVector, but I had the same problems trying clojure.lang.LazilyPersistentVector.)


At this point, I've hit a wall, and would appreciate corrections and suggestions. Thanks.
...