I was writing a blog post regarding my release of a library I have been using for a while now but have recently cleaned up, but it got too wordy. I will post it as well (perhaps already have), but I wanted to have a short post showing the capabilities of this library. That way people might actually be interested enough to download the library or read the other post. So without further ado, I present Index-Mapped-Arrays:
Index-Mapped-Arrays provides a generic interface, imref, to Common Lisp types.
(imref '(a b c d e f) 2)
(imref '(((a b) (c d)) ((e f) (g h))) 1 1 1)
(imref '((1 2 3) (4 5 6) (7 8 9)) 1 1)
(imref #2A((1 2 3) (4 5 6) (7 8 9)) 1 1)
(let ((ima '((1 2 3) (4 5 6) (7 8 9))))
(setf (imref ima 1 1) 'set)
ima )
(let ((ima #2A((1 2 3) (4 5 6) (7 8 9))))
(setf (imref ima 1 1) 'set)
ima )
As the name implies, you can map indices. Mapping indices allows you to do things like pull vectors out of matrices and grab sub-regions of arrays.
(defparameter *arr* '((1 2 3) (4 5 6) (7 8 9)))
(row-vector *arr* 1)
(column-vector *arr* 1)
(let ((*print-readably* t))
(print (column-vector *arr* 1)) )
(submatrix *arr* 0 1 3 2)
(row-vector (submatrix *arr* 0 1 3 2) 1)
Note that IMA tries its best to give you a native data structure back, but if it can't, it gives to an instance of class index-mapped-array which emulates the proper mapping.
But that's not all. Index maps are themselves setfable places.
(setf (column-vector *arr* 0) '(a b c))
*arr*
(defparameter *subarr* (submatrix *arr* 1 1 2 2))
(setf (row-vector *subarr* 0) '(d e))
*subarr*
*arr*
And to get all of this for any data structure that can be accessed via a list of integer indices, all you have to do is specialize the methods, ima-dimensions, ima-dimension, imref, and (setf imref) (or if your data structure is immutable immod). Do this, and everything above just works, and it's portable so it should work everywhere the spec is obeyed (I test on SBCL, CMUCL, CCL, CLISP, ECL, and ABCL. All but ABCL work).
And don't think that this is only useful for blocks of data. It's a bit more flexible than that. Observe:
(defclass point () ((x-value :initarg :x-value :initform 0 :accessor x-of)
(y-value :initarg :y-value :initform 0 :accessor y-of)
(z-value :initarg :z-value :initform 0 :accessor z-of) ))
(defmethod print-object ((point point) str)
(format str
"#<Point: x=~A y=~A z=~A>"
(x-of point)
(y-of point)
(z-of point) ))
(make-instance 'point :x-value 1 :y-value 2 :z-value 3)
(defmethod ima-dimensions ((ima point))
(list 3) )
(defmethod ima-dimension ((ima point) n)
3 )
(defmethod imref ((point point) &rest i)
(case (first i)
(0 (x-of point))
(1 (y-of point))
(2 (z-of point))
(otherwise (error "Index ~S out of bounds" i)) ))
(defmethod (setf imref) (new-value (point point) &rest i)
(case (first i)
(0 (setf (x-of point) new-value))
(1 (setf (y-of point) new-value))
(2 (setf (z-of point) new-value))
(otherwise (error "Index ~S out of bounds" i)) )
new-value )
(let ((point (make-instance 'point :x-value 1 :y-value 2 :z-value 3)))
(iter (for i below 3)
(collect (imref point i)) ))
For extra functionality, and you do want it, you can use the def-unmapper macro and specialize the make-ima-like method which will tell IMA how to change other IMAs into this kind of IMA, and how to make an another IMA like this one so we can fill it with data, respectively.
(defmethod make-ima-like ((point point) &key &allow-other-keys)
(make-instance 'point) )
(def-unmapper point (ima)
(when (or (< 1 (length (ima-dimensions ima)))
(< 3 (ima-dimension ima 0)) )
(error "Can only map a vector with 3 or fewer elements to a point") )
(let ((point (make-instance 'point)))
(setf (a-of point) (imref ima 0)
(b-of point) (imref ima 1)
(c-of point) (imref ima 2) )
point ))
(let ((point1 (make-instance 'point :x-value 1 :y-value 2 :z-value 3))
(point2 (make-instance 'point :x-value 3 :y-value 2 :z-value 1)) )
(list (v:dot point1 point2)
(v:cross point1 point2)
(v:+ point1 point2)
(v:- point1 point2) ))
(10 #<Point: x=-4 y=8 z=-4> #<Point: x=4 y=4 z=4> #<Point: x=-2 y=0 z=2>)
This works because my vector arithmetic library uses IMA to access the elements of its arguments, and uses make-ima-like to make the structures that it returns to the caller. The unmapper is used if you need to get some mapped data into a equivalent form but without any of the index mappings.
And if you thought that was all, I just added a new feature, a map simplifier. This means that in certain simple cases, multiple levels of mappings are reduced to a single map. For instance, taking a transpose of a transpose will return the original array. This means there is less to fear when it comes to using a mapping as part of long recursion or iteration.
(let ((ima '((1 2) (3 4))))
(iter (for i below 1000)
(setf ima (transpose ima)) )
ima )
(let ((ima '((1 2) (3 4))))
(iter (for i below 1001)
(setf ima (transpose ima)) )
ima )
(defun count-items (item-to-count ima)
(cond ((= 0 (ima-dimension ima 0))
0 )
((eql item-to-count (imref ima 0))
(+ 1 (count-items item-to-count (subvector ima 1))) )
(t (count-items item-to-count (subvector ima 1)) )))
(let ((ima (column-vector
(iter (for i below 10000)
(collect (list 1 (alexandria:random-elt '(not-it it)))) )
1 )))
(time (count-items 'it ima)) )
(let ((ima (column-vector
(iter (for i below 10000)
(collect (list 1 (alexandria:random-elt '(not-it it)))))
1 ))
(ima::*simplify* nil) )
(time (count-items 'it ima)) )
Also, if you want to exploit your data structures capabilities, you can by specializing the mapping methods. But you only need bother if you want to try and eke out some extra performance for your particular case.
It's even integrated with Iterate (although more can be done here).
(iter (for el in-ima '((1 2) (3 4)))
(collect el) )
(iter
(for col in-column-vectors-of '((1 2 3) (4 5 6) (7 8 9)))
(for row in-row-vectors-of '((1 2 3) (4 5 6) (7 8 9)))
(collect (v:dot col row)) )
Basically, I wanted to share this with the community. I've been using it for a couple years and I really like it. Maybe others out there will as well. The code is on my github page and is released under the Lesser Lisp GPL.