Why isn't there a non-! version of deleteat!()?

I just used the following bit of code in an Advent of Code puzzle, to sequentially alter a vector by removing one of its elements in turn:

deleteat!(copy(r), i) for i in 1:length(r)

I believe I had to use copy() here, because the only available version of deleteat() is the modifying version with !.

I get that there are various workarounds to this, but my n00b question is just simply: why would there be only a modifying function in base Julia, and no ‘standard’ version of the function available? (I think there are lots of other functions that seem to only have ! versions, too).

Why doesn’t Julia want me to do

deleteat(r, i) for i in 1:length(r)

?

I’m used to being able to do this in R with [ and a negative index, like this:

lapply(seq_along(x), \(i) x[-i])

but I don’t think Julia supports negative indices like this?

Thanks!

1 Like

There are packages that will do this, e.g. see InvertedIndices.jl or the @delete macro from Accessors.jl.

See also previous discussions:

2 Likes

Thank you @stevengj - I haven’t even started to explore the world of Julia packages yet! Good to know.

Note, if you’re only deleting from the front or the back of an array (repeatedly, even for any order of interleaving such), then you may consider using a view, that only makes it look like you deleted anything. (push!, and pushfirst!, even, are also often fast, i.e. with a constant factor. See sizehint!.)

But in general if you delete! from or insert! info an array anywhere else it’s inherently a mutating operation, and ! signals that.

Even for:

I.e. there ! is not used, since you make a copy that is then mutated (at least conceptually), so not having ! is correct, but somewhat misleading if you think non-mutation is inherently faster. deleteat! can be faster than it, because it doesn’t need to copy everything, maybe only shift a few elements, at worst half of them.

It doesn’t since it is slower, means inherently a conditional test.

It’s of course good to explore packages, please do, and make PRs to Julia docs if you feel something missing or can be clarified, e.g. if they should propose some package to look at. In general Julia is fast[est] [EDIT: sorry, need to heavily qualify fastest, to the point of best not saying it…, what I meant to say Julia is always fast, and with some exceptions as fast as possible, not meaning outside code can never be as fast] with what you get in the standard library. There are some exceptions, e.g. for Rationals and BigFloat, but no other types I recall (at least not the primitive ones). Well there are also faster String types available (for e.g. limited sized-strings, same as the faster fixed-sized arrays live in different packages), but eventually Julia’s should have short-string optimization, as part of Julia’s default String (IMHO).

1 Like

No. The whole point of Julia is that the standard library is not privileged — user defined types and functions can be just as fast as “built-in” ones.

There are lots of packages implementing high performance functions, often beating compositions of stdlib functions!

The simple fact is that the standard library is finite. There are plenty of useful functions it doesn’t contain. And there are downsides to including something in the standard library as opposed to a package. Not only can package development be more rapid, but packages are more free to explore different APIs than the standard library (which is locked in by backwards compatibility for all 1.x releases).

It’s not clear what the best API for “negative” indexing is so this is a good topic for packages.

6 Likes

I think it’s also worth noting that the verbs delete and pop read to me — at an English language level — as being very mutation-y. And they frequently are the names of the mutating operations in other languages.

It’s somehow easier for me to take a non-mutating verb and making it mutating with a ! than it is to go the other way.

8 Likes

Thanks everyone, very helpful answers. And not too scary - this is the “New to Julia” section after all!

I don’t know whose answer to mark as a solution, but of course I take @stevengj 's point that the Base library has to have finite limits.
Which is as good an answer to my “why?” question as any.

For what it’s worth, the motivation behind the question is really trying to smooth out the differences between my R-shaped mental model and the way things are in Julia. On the whole I have found it pretty easy to move across, so far.

I was thinking that the modifying functions with ! would tend to be variants of other ‘vanilla’ functions that don’t have the !. But that is not the case.

In R, as I put in my example above, I can iterate over x to produce various versions of x[-i] but x will still be the same. I now know some more ways to replicate that in Julia - thanks again.

One of my favorite things about Julia is how easy it is to make a new AbstractArray to perform some specific functionality. Just take a quick look at the array interface and then you can implement your own in just a few lines:

struct VectorExcept{T, V<:AbstractVector{T}} <: AbstractVector{T}
	v::V
	except::Int
	# TODO: constructor should check `Base.require_one_based_indexing(v)`
end

# here I treat out-of-bounds `except` as nothing excepted
Base.IndexStyle(::Type{<:VectorExcept}) = IndexLinear()
Base.size(x::VectorExcept) = (size(x.v,1) - (1 <= x.except <= size(x.v,1)),)
Base.getindex(x::VectorExcept, i::Int) = getindex(x.v, i + (1 <= x.except <= i))
Base.setindex!(x::VectorExcept, y, i::Int) = setindex!(x.v, y, i + (1 <= x.except <= i))
julia> VectorExcept(11:16, 2)
5-element VectorExcept{Int64, UnitRange{Int64}}:
 11
 13
 14
 15
 16

julia> [VectorExcept(1:4, i) for i in 1:4]
4-element Vector{VectorExcept{Int64, UnitRange{Int64}}}:
 [2, 3, 4]
 [1, 3, 4]
 [1, 2, 4]
 [1, 2, 3]

This doesn’t even need to make a new array. It just reads the original one while pretending that one of the entries has been deleted. But just note that mutating the VectorExcept will mutate the underlying array too. Maybe I shouldn’t have provided setindex!?

4 Likes