Email list hosting service & mailing list manager

empty intervals Alex Shinn (21 Apr 2022 11:02 UTC)
Re: empty intervals Bradley Lucier (21 Apr 2022 16:30 UTC)
Re: empty intervals Bradley Lucier (21 Apr 2022 16:37 UTC)
Re: empty intervals Per Bothner (21 Apr 2022 19:23 UTC)
Re: empty intervals Bradley Lucier (21 Apr 2022 21:59 UTC)
Re: empty intervals Per Bothner (21 Apr 2022 22:11 UTC)
Re: empty intervals Bradley Lucier (23 Apr 2022 17:56 UTC)
Re: empty intervals Per Bothner (23 Apr 2022 19:44 UTC)
Re: empty intervals Alex Shinn (24 Apr 2022 05:03 UTC)
Re: empty intervals John Cowan (21 Apr 2022 22:56 UTC)
Re: empty intervals Alex Shinn (22 Apr 2022 09:03 UTC)

Re: empty intervals Bradley Lucier 21 Apr 2022 16:30 UTC

On 4/21/22 7:02 AM, Alex Shinn wrote:
> The notes mentions only a preference not to allow empty intervals
> without a clear rationale.  It seems to me these are useful in a number
> of situations.

I agree that "empty" arrays can be useful in some circumstances.

For example, Jens Axel brought up numpy.squeeze, which eliminates axes
that have length 1 (so they're in some sense irrelevant).  I came up
with the following code:

(define (array-squeeze a)
   (let* ((domain
           (array-domain a))
          (dim
           (interval-dimension domain))
          (all-axes
           (iota dim))
          (ks
           ;; indices of all the length-one axes
           (filter (lambda (k)
                     (= (- (interval-upper-bound domain k)
                           (interval-lower-bound domain k))
                        1))
                   all-axes))
          (length-ks
           (length ks))
          (permutation
           ;; put length-one axes first, leave the rest in order.
           (list->vector (append ks (remove (lambda (k) (memv k ks))
all-axes)))))
     (cond ((zero? length-ks)
            a)
           ((= dim length-ks)
            ;; a has one element, Numpy returns a zero-dimensional array
containing the element.
            (car (array->list a)))
           (else
            (car (array->list
                  (array-curry   ;; this array has only one element.
                   (array-permute a permutation)
                   (- dim (length ks)))))))))

Here we have two special cases because the second argument to curry must
be strictly between 0 and the number of dimensions:

1.  If there are no length-one axes, then we want to return the original
array, so the entries of the result array are conceptually
zero-dimension arrays containing each array element.  Here we need the
first case of the cond.  If we had a reasonable semantics for (curry a
(array-dimension a)) this could be avoided.

2.  If all the axes have length one, then we want to curry away all the
dimensions, leaving a scalar.  numpy returns a zero-dimensional array
with the value.  So instead of an array of zero-dimensional arrays
containing single values, we have a zero-dimensional array containing
the sole element of the array.

To think of these two special cases in terms of functions we'd be converting

(lambda (x) (+ 2 x))

to either

(lambda () (lambda (x) (+ 2 x)))

or

(lambda (x) (lambda () (+ 2 x)))

I have, in the past, tried to go over all the components of this
proposal to think about how to insert consistent treatments of empty
arrays.  Here are some thoughts:

1.  Empty lists, strings, vectors, etc., can be empty in only one way:
they always have one "dimension", but there could be zero elements in
that one dimension.  Arrays can be "degenerate" in at least two ways:
there may be no dimensions, or there may be no indices in one or more
dimensions.

2.  The case of zero dimensions seems easier to integrate into this
proposal.  A zero-dimensional array would have one value.  But would a
value be (uplifted to) a zero-dimensional array?  When would you return
a zero-dimensional array containing a value, and when the value itself?
  And would this definition be recursive?  Would a zero-dimensional
array containing a zero-dimensional array containing ... a value be the
same as that value?  Even numpy isn't consistent in how it handles these
things.  (I'm not criticizing numpy for this, I'm just saying that I'd
like to have a consistent semantics for this SRFI.)

3.  I find I understand arrays with no indices in certain dimensions
even less.  Is the array with domain (make-interval '#(3) '#(3))
different from the array with domain (make-interval '#(0) '#(0))?

So, in brief, I haven't come up with a consistent semantics.

> One case I encountered recently was in a procedure to replace a single
> column within a 2x2 matrix.  This can be done by appending the column
> along with the existing left and right sides of the matrix, but since
> they may be empty we need to special case:

I agree that this case would be simplified with empty arrays with the
right semantics.  Here's a routine I came up with:

(define (array-with-column array col vals)
   (let ((columns
          (array->list
           (array-curry
            (array-permute array '#(1 0))
            1))))
     (apply array-stack
            1
            (append (take col columns)
                    (specialized-array-reshape vals (array-domain (car
columns)))
                    (drop (+ col 1) columns)))))

Here I've transferred the "empty data structure" semantics onto lists,
which have a well-defined semantics.

I find it hard to describe "confusion" in writing, which is what I end
up with when I try to write about empty arrays, zero-dimensional arrays,
etc.