mappingcommon-lispgeneralization

Generalizing Lisp functions


For rapid prototyping purposes, I would like to start building a library of generalized versions of some basic functions provided in Common Lisp, rather than only collect (or import) special purpose functions for the task at hand. For example, the map function is moderately generalized to operate on sequences of any kind, but does not handle adjustable vectors. Writing the special purpose extension below seems sufficient for present use, but remains limited:

(defun map-adjustable-vector (function adjustable-vector)
  "Provides mapping across items in an adjustable vector."
  (let ((new-adj-vec 
          (make-array (array-total-size adjustable-vector)
                        :element-type (array-element-type adjustable-vector)
                        :adjustable t
                        :fill-pointer (fill-pointer adjustable-vector))))
    (dotimes (i (array-total-size adjustable-vector))
      (setf (aref new-adj-vec i) (funcall function (aref adjustable-vector i))))
    new-adj-vec))

What I would like to see is how to go about writing a function like this that additionally takes an output type spec (including a new 'adjustable-vector type) and allows multiple kinds of lists and vectors as input--in other words, patterned after map.

More broadly, it would be useful to understand if there are basic principles or ideas relevant to writing such generalized functions. For example, would generic methods specializing on the output type specs be a viable approach for the above function? Or, perhaps, could leveraging map itself figure in the generalization, as coredump illustrated in a previous post at Mapping into a tree of nested sequences? I don't know if there are any limits to generalization (excepting a certain inefficiency), but I would like to see how far it can go.


Solution

  • In this case, map can be thought to use make-sequence to create the resulting sequence.

    However, your question is too broad, and any answer is basically an opinion. You can make whatever you want, but essentially, the most direct approach is to extend the sequence type argument to include vectors with fill pointers and/or that are adjustable:

    ;;; For use by the following deftype only
    (defun my-vector-typep (obj &key (adjustable '*) (fill-pointer '*))
      (and (or (eql adjustable '*)
               ;; (xor adjustable (adjustable-array-p obj))
               (if adjustable
                   (adjustable-array-p obj)
                   (not (adjustable-array-p obj))))
           (or (eql fill-pointer '*)
               (if (null fill-pointer)
                   (not (array-has-fill-pointer-p obj))
                   (and (array-has-fill-pointer-p obj)
                        (or (eql fill-pointer t)
                            (eql fill-pointer (fill-pointer obj))))))))
    
    ;;; A type whose purpose is to describe vectors
    ;;; with fill pointers and/or that are adjustable.
    ;;; 
    ;;; element-type and size are the same as in the type vector
    ;;; adjustable is a generalized boolean, or *
    ;;; fill-pointer is a valid fill pointer, or t or nil, or *
    (deftype my-vector (&key element-type size adjustable fill-pointer)
      (if (and (eql adjustable '*) (eql fill-pointer '*))
          `(vector element-type size)
          ;; TODO: memoize combinations of adjustable = true/false/* and fill-pointer = t/nil/*
          (let ((function-name (gensym (symbol-name 'my-vector-p))))
            (setf (symbol-function function-name)
                  #'(lambda (obj)
                      (my-vector-p obj :adjustable adjustable :fill-pointer fill-pointer)))
            `(and (vector ,element-type ,size)
                  (satisfies ,function-name)))))
    
    (defun my-make-sequence (resulting-sequence-type size &key initial-element)
      (when (eql resulting-sequence-type 'my-vector)
        (setf resulting-sequence-type '(my-vector)))
      (if (and (consp resulting-sequence-type)
               (eql (first resulting-sequence-type) 'my-vector))
          (destructuring-bind (&key (element-type '*) (type-size '* :size) (adjustable '*) (fill-pointer '*))
              (rest resulting-sequence-type)
            (assert (or (eql type-size '*) (<= size type-size)))
            (make-array (if (eql type-size '*) size type-size)
                        :element-type (if (eql element-type '*) t element-type)
                        :initial-element initial-element
                        :adjustable (if (eql adjustable '*) nil adjustable)
                        :fill-pointer (if (eql fill-pointer '*) nil fill-pointer)))
          (make-sequence resulting-sequence-type size
                         :initial-element initial-element)))
    
    ;; > (my-make-sequence '(my-vector :adjustable t) 10)
    
    (defun my-map (resulting-sequence-type function &rest sequences)
      (apply #'map-into
             (my-make-sequence resulting-sequence-type
                               (if (null sequences)
                                   0
                                   (reduce #'min (rest sequences)
                                           :key #'length
                                           :initial-value (length (first sequence))))
             function
             sequences)))
    
    ;; > (my-map '(my-vector :adjustable t) #'+ '(1 2 3) '(3 2 1))
    

    There are many other choices, like simply providing the target sequence explicitly (map-into), providing a sequence factory function with specific arguments or using a specializable generic function (if possible), using a wrapper object that decides how to store elements internally (e.g. if it is sorted, perhaps use a tree; if it has few elements, perhaps use conses) and that can return any kind of sequence.

    Really, this is such an open-ended question, it depends mostly on your needs and/or imagination.