4

I'd like to build a function, which, given a 2D matrix and some element from that matrix, will return the indexes of the element's position:

    (get-indices [[1 2 3] [4 5 6] [7 8 9]] 6)
    ;=> [1 2]

which, given back to get-in, will return the element itself:

    (get-in [[1 2 3] [4 5 6] [7 8 9]] [1 2])
    ;=> 6

I wanted the function (get-indices) to be fast, so I was thinking about doing a macro which will expand to something similar to the (cond ...) part of this function (but generic for every 2D matrix of size NxN):

      (defn get-indices
        [matrix el]
        (let [[[a b c] [d e f] [g h i]] matrix]
          (cond
            (= a el) [0 0]
            (= b el) [0 1]
            (= c el) [0 2]
            (= d el) [1 0]
            (= e el) [1 1]
            (= f el) [1 2]
            (= g el) [2 0]
            (= h el) [2 1]
            (= i el) [2 2])))

I came up with this macro:

      (defmacro get-indices
        [matrix el]
        (let [size            (count matrix)
              flat            (flatten matrix)
              compare-parts   (map #(list '= % el) flat)
              indices         (for [x (range size) y (range size)] [x y])]
           (cons 'cond (interleave compare-parts indices))))

It seemed just nice... But when called with var, not a direct value, it throws an exception:

      (def my-matrix [[1 2 3] [4 5 6] [7 8 9]])

      (get-indices my-matrix 6)
      ;=> java.lang.UnsupportedOperationException: count not supported on this
      ;   type: Symbol (NO_SOURCE_FILE:0)

To me it seems like the symbol "matrix" isn't resolved to value at macro expansion time or something like that, but I'm absolute beginner in macros...

How can I make this macro to work also with vars as arguments?

I was also thinking about using syntax-quote etc., but I'd like to avoid having (let ...) as a part of the macro output and also didn't know how to implement (interleave compare-parts indices) within the syntax-quote....

1 Answer 1

10

Writing this as a macro is a disastrous choice. As a function it's pretty simple, and more efficient than what you wanted your macro to expand to anyway:

(defn get-indices [matrix el]
  (let [h (count matrix), w (count (first matrix))]
    (loop [y 0, x 0, row (first matrix), remaining (rest matrix)]
      (cond (= x w) (recur (inc y) 0 (first remaining), (rest remaining))
            (= y h) nil
            (= (first row) el) [y x]
            :else (recur y (inc x) (rest row) remaining)))))

Conversely, as a macro it is simply impossible. Macros are for writing code at compile-time - how could you generate the cond-based code for 2D matrices at compile time, if you don't know the matrix's size until runtime?

Sign up to request clarification or add additional context in comments.

5 Comments

Note you can write this more easily with repeated get-in calls - I chose to walk through the matrix a row and an element at a time to avoid random accesses and (hopefully) improve performance, given that you seemed to care.
Thanks for clarification and the function! It's really fast (though seems that not faster than the intended - and impossible to write - macro ;-). Just one question: what random accesses did you mean?
Oh, maybe I already see what you meant: Calling get-in on increasing indices and thus random-accessing the matrix, didn't you?
And also just wanted to note that the third cond line should return [y x], to be compatible with input vector of get-in :-)
I fixed the [x y] thing. Also, try making a 1000x1000 matrix and testing your original version vs mine - you let-bind all million elements before testing any of them, while I test the very first one, find it matches, and return immediately without bothering with the others.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.