Conformal array displacement

Tagged as lisp

Written on 2020-06-04 by Daniel 'jackdaniel' KochmaƄski

In Common Lisp it is possible to displace one array to another. This is a useful feature which allows reusing the same memory for different array shapes. On LispM it was possible to displace arrays conformally and treat array as a multi-dimensional object instead of a continuous memory block.

It is said that one array is worth thousand of strings. Let's illustrate the difference with an example:

ARRAY*> (defparameter *arr* (make-array '(8 8) :initial-element 0))
*ARR*
ARRAY*> *arr*
#2A((0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0))
ARRAY*> (defparameter *dis*
          (make-array '(4 4)
                      :displaced-to *arr*
                      :displaced-index-offset
                      (array-row-major-index *arr* 2 2)))
*DIS*
ARRAY*> (loop for i from 0 below (array-total-size *dis*)
              do (setf (row-major-aref *dis* i) 1))
NIL
ARRAY*> *arr*
#2A((0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0)
    (0 0 1 1 1 1 1 1)
    (1 1 1 1 1 1 1 1)
    (1 1 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0))

Had the array *dis* been displaced conformally, the result would be different:

ARRAY*> (defparameter *arr* (make-array '(8 8) :initial-element 0))
*ARR*
ARRAY*> (defparameter *dis*
          (make-array '(4 4)
                      :displaced-to *arr*
                      :displaced-index-offset '(2 2)))
*DIS*
ARRAY*> (loop for i from 0 below (array-total-size *dis*)
                do (setf (row-major-aref *dis* i) 1))
NIL
ARRAY*> *arr*
#2A((0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0)
    (0 0 1 1 1 1 0 0)
    (0 0 1 1 1 1 0 0)
    (0 0 1 1 1 1 0 0)
    (0 0 1 1 1 1 0 0)
    (0 0 0 0 0 0 0 0)
    (0 0 0 0 0 0 0 0))

Such displacement is especially attractive when you want to model the API to return a particular slice of an array (for instance representing a screen). From that there is only one step further to add a multi-dimensional fill pointer. It serves the same purpose as for vectors. In this post I'll sketch a hack which implements something resembling arrays which are conformally displaced.

First I'll shadow array symbols which will be implemented. Package meant for consumption is named eu.turtleware.hacks.array* while the actual code is put in the package eu.turtleware.hacks.array*.implementation.

(defpackage #:eu.turtleware.hacks.array*
  (:use)
  (:export #:array*
           ;; Constructors
           #:make-array #:adjust-array
           ;; Predicates
           #:arrayp
           #:array-in-bounds-p
           #:adjustable-array-p
           #:array-has-fill-pointer-p
           ;; Accessors
           #:row-major-aref #:aref
           ;; Readers
           #:array-dimensions #:array-dimension #:array-rank
           #:array-element-type #:array-displacement
           #:array-total-size #:array-row-major-index))

(defpackage #:eu.turtleware.hacks.array*.implementation
  (:use #:cl #:eu.turtleware.hacks.array*)
  (:shadowing-import-from #:eu.turtleware.hacks.array*
                          #:array*
                          ;; Constructors
                          #:make-array #:adjust-array
                          ;; Predicates
                          #:arrayp
                          #:array-in-bounds-p
                          #:adjustable-array-p
                          #:array-has-fill-pointer-p
                          ;; Accessors
                          #:row-major-aref #:aref
                          ;; Readers
                          #:array-dimensions #:array-dimension #:array-rank
                          #:array-element-type #:array-displacement
                          #:array-total-size #:array-row-major-index))
(in-package #:eu.turtleware.hacks.array*.implementation)

I don't particularly care here about performance and consing in this implementation, because the conformal displacement should be implemented by Common Lisp vendors. They could leverage non-portable parts of the array implementation (i.e weak references to arrays which are displaced to the array). Most functions are generic and each will works for "real" arrays too.

array* is a wrapper which has four slots. array is the array to which we displace to, start and fillp define a slice of the array, and the flag inner indicates whether the array is not shared.

(defclass array* ()
  ((array :initarg :array :accessor %array)
   (start :initarg :start :accessor %start)
   (fillp :initarg :fillp :accessor %fillp)
   (inner :initarg :inner :accessor %inner)))

Some generic functions are very mundane. Macro define-wrapper is defined for such cases.

(defmacro define-wrapper (name (array-var &rest args) &body body)
  (let ((cl-name (find-symbol (symbol-name name) (find-package 'cl))))
    `(defgeneric ,name (,array-var ,@args)
       (:method ((,array-var cl:array) ,@args)
         (,cl-name ,array-var ,@args))
       (:method ((,array-var array*) ,@args)
         ,@body))))

Predicates are straightforward. arrayp works on any object, the rest works only for arrays.

(defgeneric arrayp (array)
  (:method (array) nil)
  (:method ((array cl:array)) t)
  (:method ((array array*)) t))

(define-wrapper adjustable-array-p (array)
  t)

(define-wrapper array-has-fill-pointer-p (array)
  t)

(defgeneric array-in-bounds-p (array &rest subscripts)
  (:method ((array cl:array) &rest subscripts)
    (apply #'cl:array-in-bounds-p array subscripts))
  (:method ((array array*) &rest subscripts)
    (loop for start in (%start array)
          for fillp in (%fillp array)
          for len = (- fillp start)
          for sub in subscripts
          unless (typep sub `(integer 0 ,len))
            do (return-from array-in-bounds-p nil)
          finally (return t))))

Readers are also trivial. array-displacement returns the third value indicating the last index in the displaced-to array.

(define-wrapper array-element-type (array)
  (array-element-type (%array array)))

(define-wrapper array-rank (array)
  (length (%start array)))

(define-wrapper array-dimensions (array)
  (mapcar #'- (%fillp array) (%start array)))

(define-wrapper array-dimension (array axis-number)
  (- (nth (%fillp array) axis-number)
     (nth (%start array) axis-number)))

(define-wrapper array-total-size (array)
  (reduce #'* (array-dimensions array)))

(define-wrapper array-displacement (array)
  (values (%array array)
          (%start array)
          (%fillp array)))

Accessors are more tricky. For aref I'll define a helper function get-real-subscripts which translates supplied subscripts to the underlying array's subscripts.

(defun get-real-subscripts (array &rest subscripts)
  (loop for sub in subscripts
        for off in (%start array)
        for flp in (%fillp array)
        for ind = (+ sub off)
        if (and (>= ind off) (< ind flp))
          collect ind into subs
        else
          do (error "Invalid index.")
        finally
           (return subs)))

Now implementing aref and (setf aref) is a matter of translating subscripts and calling the function on a displaced-to array.

(defgeneric aref (array &rest subscripts)
  (:method ((array cl:array) &rest subscripts)
    (apply #'cl:aref array subscripts))
  (:method ((array array*) &rest subscripts)
    (apply #'cl:aref
           (%array array)
           (apply #'get-real-subscripts array subscripts))))

(defgeneric (setf aref) (new-value array &rest subscripts)
  (:argument-precedence-order array new-value)
  (:method (new-value (array cl:array) &rest subscripts)
    (apply #'(setf cl:aref) new-value array subscripts))
  (:method (new-value (array array*) &rest subscripts)
    (apply #'(setf aref)
           new-value
           (%array array)
           (apply #'get-real-subscripts array subscripts))))

To access the array with the row major index a function which reconstructs subscripts from the integer is necessary. A reverse operation computes the row major index from subscripts and it is of course array-row-major-index.

(defun row-major-index-to-subscripts (array index)
  (loop with ind = index
        with sub
        for rem on (array-dimensions array)
        do (multiple-value-setq (sub ind)
             (truncate ind (reduce #'* (cdr rem))))
        collect sub))

(defgeneric array-row-major-index (array &rest subscripts)
  (:method ((array cl:array) &rest subscripts)
    (apply #'cl:array-row-major-index array subscripts))
  (:method ((array array*) &rest subscripts)
    ;; Q: Can we do better?; A: Of course we can!
    ;; Q: Why won't we?;     A: Too much hassle!
    (loop for rem on (array-dimensions array)
          for sub in subscripts
          summing (* sub (reduce #'* (cdr rem))))))

Having row-major-index-to-subscripts implemented, accessors row-major-aref and (setf row-major-aref) are easy:

(define-wrapper row-major-aref (array index)
  (apply #'aref array (row-major-index-to-subscripts array index)))

(defgeneric (setf row-major-aref) (new-value array index)
  (:argument-precedence-order array index new-value)
  (:method (new-value (array cl:array) index)
    (setf (cl:row-major-aref array index) new-value))
  (:method (new-value (array array*) index)
    (apply #'(setf aref) new-value array
           (row-major-index-to-subscripts array index))))

As noted before, I don't care about offsetting computations to compilation time. However if I did I could have made an interesting blunder (which can be avoided by the implementation made by a vendor): write a hash function which takes row major index of the array and returns row major index of the displaced-to array. That would make access faster. The problem is that when the displaced-to array is adjusted, the hash function may be invalid because array dimensions change and there is no portable way to detect that - each function would need to explicitly check the displaced-to array dimensions if they are the same as previously.

Now it is time to implement constructors make-array and adjust-array. They are quite similar, especially when it comes to validating parameters. The next three functions are utilities shared by both. check-conformal-args validates arguments. Most notably it checks whether displacement arguments have the same arity as the array rank.

(defun check-conformal-args
    (dimensions initial-element initial-contents
     fill-pointer displaced-to displaced-index-offset)
  (cond ((and (not displaced-to) displaced-index-offset)
         (error "Can't specify ~s without ~s."
                :displaced-index-offset :displaced-to))
        ((and displaced-to (or initial-element initial-contents))
         (error "~s and ~s are mutually exclusive with ~s."
                :initial-element :initial-contents :displaced-to))
        ((and (consp fill-pointer)
              (/= (length fill-pointer) (length dimensions)))
         (error "~s must have the same length as DIMENSIONS."
                :fill-pointer))
        ((and (consp displaced-index-offset)
              (/= (length displaced-index-offset) (length dimensions)))
         (error "~s must have the same length as DIMENSIONS."
                :displaced-index-offset))))

Slots start and fillp are expressed as indexes of the displaced-to array (usually fill-pointer is expressed in vector indexes). I use a helper function to return lists for displaced-index-offset and fill-pointer which are computed based on array dimensions. This function assumes, that arguments are already validated with check-conformal-args.

(defun fix-displacement (dimensions displaced-index-offset fill-pointer)
  ;; Correct the FILL-POINTER and the DISPLACED-INDEX-OFFSET. Both
  ;; should be expressed in the destination array indexes.
  (cond ((and (atom fill-pointer)
              (atom displaced-index-offset))
         (setf displaced-index-offset
               (make-list (length dimensions) :initial-element 0))
         (setf fill-pointer dimensions))
        ((atom fill-pointer)
         (setf fill-pointer (mapcar #'+ displaced-index-offset dimensions)))
        ((atom displaced-index-offset)
         (setf displaced-index-offset (mapcar #'- fill-pointer dimensions)))
        (t
         (setf fill-pointer (mapcar #'+ displaced-index-offset fill-pointer))))
  (values displaced-index-offset fill-pointer))

The last function checks whether final indexes have valid order:

(defun check-indexes (dimensions displaced-index-offset fill-pointer)
  (every #'<=
         (make-list (length dimensions) :initial-element 0)
         displaced-index-offset
         fill-pointer
         (mapcar #'+ displaced-index-offset dimensions)))

make-array may construct three different objects:

  • cl:array instance when there is no conformal displacement
  • array* instance with inner=Y for multi-dimensional fill-pointer
  • array* instance with inner=N for conformally displaced array
(defun make-array (dimensions &rest args
                   &key
                     (element-type t)
                     initial-element
                     initial-contents
                     adjustable
                     fill-pointer
                     displaced-to
                     displaced-index-offset)
  (declare (ignore element-type adjustable))
  (when (and (atom displaced-index-offset)
             (atom fill-pointer)
             (not (typep displaced-to 'array)))
    (return-from make-array
      (apply #'cl:make-array dimensions args)))
  (check-conformal-args dimensions initial-element initial-contents
                        fill-pointer displaced-to displaced-index-offset)
  (when (null displaced-to)
    ;; implies that D-I-O is NIL and that F-P is CONS
    (remf args :fill-pointer)
    (return-from make-array
      (make-instance 'array*
                     :array (apply #'cl:make-array dimensions args)
                     :start (make-list (length dimensions) :initial-element 0)
                     :fillp fill-pointer
                     :inner t)))
  (multiple-value-setq (displaced-index-offset fill-pointer)
    (fix-displacement dimensions displaced-index-offset fill-pointer))
  ;; Assert the indice correctness.
  (if (and (check-indexes dimensions displaced-index-offset fill-pointer)
           (every #'<= fill-pointer (array-dimensions displaced-to)))
      (make-instance 'array*
                     :array displaced-to
                     :start displaced-index-offset
                     :fillp fill-pointer
                     :inner nil)
      (error "Invalid FILL-POINTER or DISPLACED-INDEX-OFFSET specification.")))

adjust-array, unless called on cl:array (in which case it calls cl:adjust-array), always preserves the array identity. When the array is confromally displaced to another one (inner=NIL), or the parameter displaced-to is not NIL, A new array is created with make-array and slots are copied from the result.

Otherwise the displaced-to array is private, so it is possible to adjust start and fillp. If the result does not fit in the displaced-to array, it is adjusted too.

(defun adjust-array (array dimensions &rest args
                     &key
                       element-type
                       initial-element
                       initial-contents
                       fill-pointer
                       displaced-to
                       displaced-index-offset)
  (declare (ignore element-type))
  (etypecase array
    (cl:array
     (apply #'adjust-array array dimensions args))
    (array*
     (when (or (not (%inner array)) displaced-to)
       (let ((arr (apply #'make-array array dimensions args)))
         (if (typep arr 'array*)
             (setf (%array array) (%array arr)
                   (%start array) (%start arr)
                   (%fillp array) (%fillp arr)
                   (%inner array) nil)
             (setf (%array array) arr
                   (%start array) (make-list (length dimensions) :initial-element 0)
                   (%fillp array) (array-dimensions arr)
                   (%inner array) t))))
     (check-conformal-args dimensions initial-element initial-contents
                           fill-pointer displaced-to displaced-index-offset)
     (setf displaced-to (%inner array))
     (multiple-value-setq (displaced-index-offset fill-pointer)
       (fix-displacement dimensions displaced-index-offset fill-pointer))
     ;; Assert the indice correctness.
     (unless (check-indexes dimensions displaced-index-offset fill-pointer)
       (error "Invalid FILL-POINTER or DISPLACED-INDEX-OFFSET specification."))
     (unless (every #'<= fill-pointer displaced-to)
       (remf args fill-pointer)
       (remf args displaced-index-offset)
       (setf displaced-to (apply #'adjust-array displaced-to fill-pointer args)))
     (setf (%array array) displaced-to
           (%start array) displaced-index-offset
           (%fillp array) fill-pointer)
     array)))

That's all. As noted before, this is a mere sketch, but works fairly good. I've written it to incorporate in the charming-clim tutorial, but I've decided to not complicate description too much. Still I think that it is an interesting insight so I've decided to make it into a separate post. In the future I'd like to incorporate this very cool feature into the Embeddable Common Lisp.

If you feel like supporting me with my FLOSS contributions and blogging you may become my patron.