0

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.

Community
  • 1
  • 1
davypough
  • 1,847
  • 11
  • 21
  • `map` handles adjustable vectors just fine. – sds Mar 07 '17 at 17:08
  • @sds, the adjustability of `map`'s resulting vectors is undefined. – acelent Mar 07 '17 at 19:11
  • Since [`adjust-array`](http://clhs.lisp.se/Body/f_adjust.htm) works on non-adjustable arrays, I think the difference is inconsequential. – sds Mar 07 '17 at 19:54
  • @sds, except that if the array is adjustable, `adjust-array` will return that same array. – acelent Mar 08 '17 at 10:18
  • Yes, the point was only that `map` destroys the fill-pointer. But I'm still interested in how to generalize built-in functions. (Another similar example I've been thinking about is `coerce`, which seems it could be extended to cover other reasonable "equivalent" typed-object conversions.) – davypough Mar 08 '17 at 16:52

1 Answers1

0

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.

acelent
  • 7,965
  • 21
  • 39
  • I'd like to study this, thanks. In the meantime, do you know how can I get the source code for the `map` function, for example on SBCL? `function-lambda-expresssion` just returns `nil`. – davypough Mar 10 '17 at 19:03
  • SBCL is open-source, its code is here: [https://github.com/sbcl/sbcl](https://github.com/sbcl/sbcl). – acelent Mar 10 '17 at 19:15
  • Appreciate the SBCL `map` function reference, but it looks like the code is quite dependent on sequence structure, and so may not exhibit much flexibility in this area. – davypough Mar 10 '17 at 22:04
  • I think I understand your basic approach here, and it seems to involve a fair amount of reworking/extension of the `sequence` concept embedded in the code. Although this would surely work well for each specific extension, it makes me wonder if there isn't a more "generalizable" way to extend a built-in function. It seems conceivable that someone might want to "map" over a dotted list or many other kinds of containers, as well. – davypough Mar 10 '17 at 22:05
  • Could generic functions be adapted for each specialization, say for a functional extension defined as `map+`, that uses `map` for all the normal sequence-related argument specializations, but calls other `map+` generic methods for alternate arguments? Do you know of a library that adds generic function capabilities like these, or is this infeasible given given the underlying CLOS architecture? – davypough Mar 10 '17 at 22:05
  • Check this out: [https://github.com/fare/lisp-interface-library](https://github.com/fare/lisp-interface-library). – acelent Mar 10 '17 at 23:50