Since end is, in general, the keyword for the last portion of a block in Julia, its special behavior as the last index is limited to the indexing syntaxes. We simply cannot extend support to general purpose functions since we don’t know which arguments are the array and which arguments are the indices.
You can check out the @view macro on how to make this kind of macro:
What does the end do in x[f(y, end)]? Is it grabbing the last index in x? Or is f a special-cased function that allows the expansion of indices with respect to y such that it’s the last index in y? What if I had assigned const f = deleteat! earlier in my file?
This transformation occurs before Julia even knows what functions are what. The parser knows the API of x[i] because that’s syntax. The API of a general function f(y, i) isn’t known, and at parse-time, Julia doesn’t even know which module f belongs to, let alone which argument is an array and which argument is an index. And if it’s hard for Julia to figure out what it should be doing, well then good luck to all mortal programmers trying to read the file!
These are the same if the array’s indices are 1:length(a) but that’s not always the case, e.g. for OffsetArrays. On the other hand, the first and last indices of an array are always given by the firstindex and lastindex functions (that’s what they’re for).
Yes - this is right - I did not think about it when answering first. You cannot put end in expression even if you do not evaluate it - except in indexing. Right?
But one could imagine using some other symbol, different than end, and process it replacing by lastindex call using a macro - like @splice!(v, 1:END) that would be transformed to splice!(x, 1:lastindex(v)) (but probably this is not worth the effort and the macro itself would not be simple unless you restrict yourself to a narrow range of use cases).
But where do you get the object that lastindex is supposed to be called on? In a[...] syntax, we know: it’s the thing before the [—the thing that you’re indexing into. In function call syntax how do you know what you’re supposed to take the last index of? One of the arguments of the function? The first one? That seems quite arbitrary and non-general.
There are also cases where you want to replace end in indexing with lastindex(a, d) where d is the index of the dimension into which you are indexing. In array indexing syntax, it’s completely clear what dimension you’re indexing into so that’s not a problem. How are you supposed to know if end in a general function call is supposed to lower to lastindex(a) or lastindex(a, d) for some d? And how would you figure out d?
Oh, you’re right — of course you couldn’t do @upend deleteat!(A, end-1). The @view macro works because it works on a syntax where end is supported: @view A[end]. This didn’t occur to me initially, either!
For what it’s worth, there’s also a package (I think by Mauro) that uses a special End() type which supports some deferred operations and evaluates to the correct thing once it hits an indexing operation.
There’s a version of all of this where end (and begin) in indexing is evaluated lazily, which could allow this to work, but that becomes really complex and hard to reason about very quickly and to be fully general, it needs to implement a complete lazy evaluation system. The current syntax can be implemented completely during lowering which makes it pretty simple.
There are two possible solutions (the key is that we do not call a function but a macro):
Use the syntax @splice! v[1:end] and now the macro @splice knows exactly that end refers to v and - in this case would rewrite this expression as splice!(v, 1:lastindex(v)); here the key difficulty in writing the macro would be to only replace the outermost end, so that we correctly handle something like @splice! v[1:end][a[end]:end] as h=v[1:end]; splice!(h,a[end]:lastindex(h)); where h would get generted by gensym
Use the syntax @splice(v, 1:END) (here we need a special sentinel other than end). In this case the macro would rewrite this again as splice!(v, 1:lastindex(v)); in this case the macro is simpler to write as we have to assign the value of an expression in v to some new variable using gensym, call it h, and then rewrite END by lastindex(h) everywhere inside;
Also as splice! works only on vectors it is enough that we handle lastindex(x).