Why `front` and `tail` are defined for only `Tuple`?

https://docs.julialang.org/en/v1/base/collections/#Base.front

I expect below, but MethodError raised:

julia> x = [3, 5, 7, 4, 0, 9];
julia> Base.front(x)
[3,5,7,4,0]

Of course x[Not(end)] is also great choice if we are using DataFrames.jl, or just x[(end-1):end] is OK. But maybe front and tail are very useful like first and last. Is there any reason why the functions are not defined for other collections, for instance, performance issue?

I don’t know about historical reasons, but generalizing this is a little tough. For tuples, they are not allocating - the compiler knows exactly the type of Tuple it will return based on the argument. But to do this for a Vector, you’d either need to make a new container or return a surprise View.

Edit: one other option is a mutation with pop! or popfirst! … But we already have those functions :sweat_smile:

first and last apply generically to iterators. Base.front is basically the same as first, but Base.tail might not work as expected for all iterators.

1 Like

Be very careful with this Not thing, assuming it comes from InvertedIndices.jl. Its implementation is very inefficient, compare:

# manual indexing, baseline:
julia> @btime $x[begin:end-1]
  109.519 ns (1 allocation: 96 bytes)

# Not(): 30x slower!
julia> using InvertedIndices
julia> @btime $x[Not(end)]
  2.961 μs (35 allocations: 1.14 KiB)

# there's a readable zero-overhead solution in Accessors:
# @delete x[end]
# or
# @delete last(x)
julia> using Accessors
julia> @btime @delete $x[end]
  95.899 ns (1 allocation: 112 bytes)
julia> @btime @delete last($x)
  95.795 ns (1 allocation: 112 bytes)

But yeah, anyway a more general front/tail in Base would be nice.

5 Likes

That last optic seems sorta crazy. What is happening there?

Wow, good to know! Thank you!! But Not is very convenient :joy:… I have a question, do your alternative solution actually changes original x?

I’ve only seen these methods used commonly for recursive inlined Tuple iteration. If comprehension was type stable for tuples it’d probably be used instead.

No magic, just https://github.com/JuliaObjects/Accessors.jl/blob/master/src/functionlenses.jl#L6 (:

2 Likes

Well, Accessors are also very convenient — but more general and efficient!

No, all Accessors operations create a new object and keep the original one intact.
So, it’s used as

...
y = @delete last(x)
...

and not as

...
@delete last(x)
...

Recently I use tensor so many times so I implement head(same to front in this thread) and tail by meta programming. I wish one could save time using my functions.

head(x) = eval(Meta.parse("$(x)[$(":, " ^ (length(size(x))-1)) 1:(end-1)]"))
tail(x) = eval(Meta.parse("$(x)[$(":, " ^ (length(size(x))-1)) 2: end   ]"))

head and tail detect last dimension of array and drop a last or first slice in the dimenson.

Example

vector:

julia> @show X = rand(0:9, 10);
X = rand(0:9, 10) = [3, 9, 0, 2, 8, 6, 8, 9, 2, 9]

julia> @show head(X);
head(X) = [3, 9, 0, 2, 8, 6, 8, 9, 2]

julia> @show tail(X);
tail(X) = [9, 0, 2, 8, 6, 8, 9, 2, 9]

matrix:

julia> X_ = rand(0:9, 3, 3)
3×3 Matrix{Int64}:
 0  8  7
 0  5  8
 6  4  1

julia> head(X_)
3×2 Matrix{Int64}:
 0  8
 0  5
 6  4

julia> tail(X_)
3×2 Matrix{Int64}:
 8  7
 5  8
 4  1

3-tensor:

julia> X__ = rand(0:1, 3, 3, 3)
3×3×3 Array{Int64, 3}:
[:, :, 1] =
 1  0  0
 1  1  1
 1  0  1

[:, :, 2] =
 1  0  1
 1  1  0
 1  0  1

[:, :, 3] =
 0  0  0
 0  1  0
 0  1  1

julia> head(X__)
3×3×2 Array{Int64, 3}:
[:, :, 1] =
 1  0  0
 1  1  1
 1  0  1

[:, :, 2] =
 1  0  1
 1  1  0
 1  0  1

julia> tail(X__)
3×3×2 Array{Int64, 3}:
[:, :, 1] =
 1  0  1
 1  1  0
 1  0  1

[:, :, 2] =
 0  0  0
 0  1  0
 0  1  1

vector of vector:

julia> head([[2, 3], [4, 6, 7], ["s"]])
2-element Vector{Vector}:
 [2, 3]
 [4, 6, 7]

julia> tail([[2, 3], [4, 6, 7], ["s"]])
2-element Vector{Vector}:
 [4, 6, 7]
 ["s"]

I’m not sure if you need to do metaprogramming for this.

julia> head(x::AbstractArray{T,N}) where {T,N} = x[ntuple(i->Colon(),Val{N-1}())..., begin:end-1]
head (generic function with 1 method)

julia> tail(x::AbstractArray{T,N}) where {T,N} = x[ntuple(i->Colon(),Val{N-1}())..., begin+1:end]
tail (generic function with 1 method)

julia> head(1:5)                                                      
 1:4

julia> tail(1:5)                                                     
 2:5
5 Likes
head1(x) = eval(Meta.parse("$(x)[$(":, " ^ (length(size(x))-1)) 1:(end-1)]"))
head2(x::AbstractArray{T,N}) where {T,N} = x[ntuple(i->Colon(),Val{N-1}())..., begin:end-1]

X = rand(5, 5, 5);

using BenchmarkTools

I have tested. Your implementation is x25 fatster than mine, thank you!

julia> @btime head1($X);
  1.783 ms (1065 allocations: 88.93 KiB)

julia> @btime head2($X);
  71.488 ns (1 allocation: 896 bytes)
1 Like