So with broadcasting dots, we've managed to excise nearly all sometimes-scalar/s…ometimes-vectorized functions in Julia to great benefit. There are two notable stragglers: `getindex` and `setindex!`. Now, since they have their own syntaxes and support so many fancy things it hasn't always been (and really still isn't) completely obvious that it should be deprecated in favor of broadcasting. But they really are just "broadcasting" scalar indexing over the cartesian product of the indices.
## The advantages
There'd be a number of advantages to spelling nonscalar indexing as some form of broadcasting. First and foremost: fusion. Forget the whole view/copy debate — straight-up fusion could be faster than either. For example, we could define the new syntax `A.[I...]` to mean `broadcast((idxs...)->A[idxs...], I...)`. Then, to extract the first 10 elements from the first column of `A`, you write `A.[1:10, 1]`. This means that you could fuse in additional operations without any temporaries or views whatsoever with, e.g., `sqrt.(A.[1:10, 1])`.
The other huge advantage is how this would then generalize these sorts of accesses to _all_ data structures, including dictionaries and more. Cf. #24019.
## The challenges and their potential solutions
Now, there are also some fairly major challenges in making it easy to express all that indexing does with a broadcasting semantic.
* **APL Indexing:** The existing `A[1:10, 1:10]` takes the cartesian product of the two indices, returning a 2-dimensional 10x10 array. I'd expect a broadcasted `A.[1:10, 1:10]` to use broadcasting's shape-matching semantic and return the diagonal. In general, APL indexing can be thought of as a broadcast where each successive index is "lifted" to a higher dimension than the sum of dimensionalities of all arguments that preceded it. We could potentially have a simple wrapper that flags arguments to be "orthogonalized" before participating in broadcast — options for spelling this wrapper could include `⟂` or `^`. Thus, `A[I, J]` becomes `A.[I, ^J]`. This is a generally useful operation... and for my purposes would completely obviate the pain from the recursive transpose/adjoint fallback removal as you could lift arguments to orthogonal dimensions _anywhere_: `f.(1:10, ^array_of_strings)`. That said, the change in default operation here from APL-like to broadcasting-like may prove to be very painful...
* **Index conversions:** The existing indexing API supports [many types of indices](https://docs.julialang.org/en/v1/manual/arrays/#man-supported-index-types-1) and even an [extensible interface for adding more](https://docs.julialang.org/en/v1/base/arrays/#Base.to_indices).
* For example, `:` is actually a _function_, but when used as an index it expands out to the entire axis. With broadcast, however, it acts like a function — as a scalar.
* Logical indexing with boolean arrays is even worse: there we have an array of trues and falses, but when used as an index it expands out to a vector of the locations of the trues. With broadcast, it behaves just like an array would, yielding the same number of trues and falses as the arrays overall shape.
Resolving this one means unfortunately giving up something fairly major: I don't believe it'll ever be possible to support all these features _and also_ support broadcast fusion through to an index computation. So while it would be oh-so-cool to have an expression like `A.[clamp.(idx .+ (-N:N), 1, end)].^2` (to examine a window about some `idx` without worrying about bounds) fuse the whole way down, it simply isn't extensible to the very next thing you'd want to have fuse: `A.[A .> 0]`. That would be a nightmare to figure out how to fuse.
* **Bounds checking:** In comparison to the other issues, this one feels much simpler, but it's still gonna be a bit of a pain. Non-scalar indexing is able to perform bounds checking at the level of the collections of indices and then perform the scalar indexing with bounds checks off. This is a _huge_ win for things like `:` (no checks), `1:10` (just check endpoints), and logical masks (just check the shape). I think this may be possible to still do — again, if we don't fuse through to the index computations. But I've not completely seen to the end of this tunnel.
* **Returned array type:** We’ll need to find a solution to #17533; `similar`’s first major use was in defining the output type for nonscalar indexing. Lots of folks specialized it for that reason, and broadcast doesn’t use it.
* **Backwards compatibility:** For the most part, this is entirely a new syntax with a straight-forward deprecation. There's one place where I was worried about a clash, though: the `@.` macro. That currently leaves indexing expressions alone, but once we introduce the `A.[]` operator, I'd expect it to dot those bracket operations. It seems like we have sufficient leeway in the macro to do some fancy detection, though, allowing us to insert appropriate handling code if any arguments are arrays and inserting deprecations appropriately.
* **Indexed Assignment:** So far I've really only talked about getindex, but all the same applies to `setindex!`, too. It actually becomes quite the advantage as the dots can tell the whole story about what's scalar and whats not right where you'd expect (adapted from the sister issue #24086):
* `A[i] = x`: Always sets `x` at the scalar index `i`.
* `A[i] .= X`: Always broadcasts `X` across the element at location `i` (always mutating the object in `A[i]`).
* `A.[I] = x`: Assigns `x` to every index in `I`.
* `A.[I] .= X`: Always broadcasts `X` across the indices selected by `I` (always mutating `A`).
## What to do:
So, bringing this all together, I propose we aim to:
* Introduce the syntax `A.[I, J, K]` to mean:
```julia
idxs = to_indices(A, (I, J, K))
broadcast((i, j, k)->A[i,j,k], idxs...)...)
```
The separation into two statements there is meaningful — that's how it'll behave in fusion. Note, too, that this _defaults_ to behaving broadcast-like (and not APL-like). I'm still not entirely sold on it, but I do think it'll unify the language and simplify things. I think this is one of those things we'll have to see how it feels in practice.
* Introduce the new "orthogonalization" syntax `f.(a, ^b, c, ^d)` to mean:
```julia
f.(a, Lifted(b, ndims(a)), c, Lifted(d, ndims(a) + ndims(Lifted(b, ndims(a))) + ndims(c))
```
This may pose some constant-propagation and type-stability challenges, but I hope they're overcome-able.
* Apply these semantics to indexed assignments over in #24086.
## References and prior discussions
This issue brings together lots of iterations of thought that have been spread about through many issues and discourse communications.
* [What to do about multivalue setindex!? (#24086)](https://github.com/JuliaLang/julia/issues/24086)
* [Broadcast Array Indexing (#19169): ](https://github.com/JuliaLang/julia/issues/19169) discusses if the array itself should participate in the broadcasting (for indexing into many arrays inside an array). [Discourse: Square Bracket Notation for Broadcasting getindex Across Array of Vectors?](https://discourse.julialang.org/t/square-bracket-notation-for-broadcasting-getindex-across-array-of-vectors/6227) requests the same thing.
* [Make indexing expressions participate in dot syntax fusion (#22858)](https://github.com/JuliaLang/julia/issues/22858): started sketching out lots of this, but defaulted to APL indexing semantics. My [last comment](https://github.com/JuliaLang/julia/issues/22858#issuecomment-338267870) is fairly negative because lifting arguments to orthogonalize them requires array wrappers — and array wrappers will allocate just like views will. But this omits the fact that many indices won't be heap-allocated (like ranges) and defaulting to broadcasting semantics will further limit the damage here. Upon further reflection I think it's a win.
* [Broadcasting pointwise indexing operator (#2591)](https://github.com/JuliaLang/julia/issues/2591) is one of the first discussions on this topic.
* [Discourse: understanding view](https://discourse.julialang.org/t/understanding-view/18286/8?u=mbauman): A discussion about the strange-ness of the sometimes-scalar sometimes-not behaviors of `getindex` as compared to `view` (which is always nonscalar).