Slicing syntax for math in Swift (tensors)

I'm currently experimenting with tensor slicing syntax in Swift, to see what is possible. I've run into limitations, and I'd like to get advice and/or report feedback for possible evolution of the language. Please let me know if this has already been discussed.

From Python/Numpy (but it shares a lot with preexisting math packages like Matlab/Scilab/Octave and others I haven't experienced so far):

tensor[:,:,1:7] // returns a 3D tensor (full ranges on first/second axis, range on last axis)
tensor[:,:,1::2] // returns a 3D tensor (same, but stride of 2 on last axis)
tensor[:,:,1:5:2] // returns a 3D tensor (another)
tensor[1,:,4:] // returns a 2D tensor (picks first axis & remove it, + range on last axis)
tensor[np.newaxis,:,:,:] // returns a 4D tensor (inserts a new axis)

I implemented all these behaviors in CeedNumerics as a test bed, it's not too bad, but I still can't get a syntax that is as legible as Python's.

tensor[n.all,n.all,1~7]
tensor[n.all,n.all,1~~2]
tensor[n.all,n.all,1~5~2]
tensor[1,n.all,4~]
tensor[n.newaxis,n.all,n.all,n.all]

Had to replace the : with ~ (colon can't be used as it is reserved). I used the same enum/operator trick that Swift uses for the UnboundedRange (...) that can be used both standalone or with arguments. But this has some limitations:

  • can't make it conform to a protocol, and thus I have to write a number of specific subscripts with all combinations (~ as 1st argument, as 2nd, and both for a 2D type)
  • can't use it when arguments is of type [Any] (which I need, because standalone ~ can't conform to a protocol.

Note that standalone ~ can be used in place of n.all in the case of matrices, as [Any] subscript can be skipped altogether.

matrix[~,1]
tensor[n.all, 1] // n.all must be used instead of ~

I'd be interested in these specific points:

  1. is there a chance that the colon syntax for tensor slicing (start:end:step, + stand alone/unary/binary variants), that is generally well accepted and used for math programming, becomes available in Swift?
  2. if not, is there hope to see improvements (removal of the UnboundedRange, or at least allow protocol conformance) to make the proposed workaround work better (and avoid the n.all constant)?
  3. Any additional feedback or discussion about the long-term plan about how to better express math stuff in Swift, or short-term tricks that can be used right away would also be appreciated.

Thank you.

Swift’s range-formation operators are ... and ..<.

It’d be interesting to have 1...100 ~ 2 or 1... ~ 2. a bit verbose (definitely can’t beat 1:2:100), but looks rather swifty imo.

slices are more than a range, there's a third argument, the step. Also, the operator needs to handle partial definition (range have that, albeit with the mentioned limitation), and ideally be very compact (single char) as they have to be used on every axis. That's why range-formation are not a great fit, although I also made them conformant to the NSliceExpression protocol, in case one wants to use them.

there are some other combinations to consider: ... ~ 2 and ...10~2 (spaces are not great in the former).

Also, I forgot to mention, but negative indices (-1 is last element) are typically supported in slices: tensor[-1:], which is another difference with Swift ranges.

I was referring to your examples that used things like 1~7 and n.all instead of 1...7 and ....

Anything that involves steps within a range should be based on the fact that there is a range, which is then stepped through.

Swift already has stride(from: 1, through: 7, by: 2), so really what you’re proposing is a shorthand spelling for that. Perhaps (1...7).stride(2), or with some clever wrapper types, simply 1...7.stride(2).

In the latter case, you would make 7.stride(2) return, say, a StridedRangeBound instance, and then overload the infix range-formation operators accordingly.

2 Likes

Could it be possible to amend the existing range syntax with additional stride operations?

tensor[..., ..., 1...7]
tensor[..., ..., (1...).stride(2)]
tensor[..., ..., (1...5).stride(2)]
tensor[1, ..., 4...]
tensor[n.newaxis, ..., ..., ...]
1 Like

my understanding is that ... wouldn't work if passed to a tensor subscript taking Any... as argument, which is what I'm currently experiencing with the standalone ~ (compiler error: ambiguous use of operator '~').

And I'm still finding it pretty verbose when compared to the other languages, which makes the code not as clear/concise. (also, no negative index support…)

tensor[:,:,1:7]
tensor[:,:,1::2]
tensor[:,:,1:5:2]
tensor[1,:,4:]
tensor[np.newaxis,:,:,:]

I don't understand why you'f want to take Any... as the argument. It looks like a code smell.

I guess that's fair. However, for better or worse, ... and ..< are already decided as range operators in Swift. No one is going to change that now. And wether n:m is better or worse than n...m is a matter of preference, I guess. The latter looks better to me as a mathematician, but I agree that it can be a little too much, when using many ranges in a row.

But it may still be possible to propose some kind of stride-operator for ranges. How about n...m:s for a range from n through m with a stride of s?

2 Likes

Thinking on it further, I think one of these would be a better spelling:

(1...7).by(2)
1...7.by(2)
1..2...7
2 Likes

Sure, I agree, but can't do it, because Swift won't let the enum's static postfix func ... conform to a common protocol. It needs to be an array that accepts Ints, ranges, slices, and the ... operator. If you have another solution, please tell.

About the latter part: I was not seeing slice as a range replacement, but rather as a specific construct to address the specifics here (including negative index). I agree there is an intersection with the range concept.