Why does Array{Int}() product Array{Int, 0} and not Array{Int, 1}


#1

When initializing an array variable (whose size is unknown at the time), I often write a= Array{Int}() or r = Array{Rational}(), and then get an error when I try to push! into it later (no method matching push!(::Array{Rational,0}, ::Rational{Int64})).

It seems to me that it will be convenient if the constructor for Array{Int64,N} where N assumed N was 1, instead of assuming it 0 when left unspecified. I realize I can always write Array{Int, 1} or even better, Vector{Int} instead, but this seems like it would be a convenient shorthand for the common case of wanting an 1-dimensional array, and more useful than getting back a 0-dimensional array.

But may be I’m missing the intent behind this, so is there a reason such a constructor call gives you back an Array{T, 0} and not Array{T, 1}?


#2

The size of a zero-dimensional array is (). That is, it has no dimensions. Thus, when you call the Array constructor with no dimensions, it instantiates a zero-dimensional array.

To construct a zero-length vector with a similar sort of shorthand, you can use Vector{Int}() instead. Or you can pass one explicit zero-length dimension like Array{Int}(0) — but that’s changing to Array{Int}(undef, 0) in 0.7.


#3

You can also use something even simpler, Int[], that’s what I always use.


#4

That kinda makes sense in linguistic terms, but in programming terms we usually don’t take missing argument x to mean x doesn’t exist or is 0, we usually expect it to have a sane default. To take a random example, when calling isapprox(3.1415926535, π), isapprox doesn’t assume rtol should be zero because it’s left unspecified, it takes a reasonable default value appropriate to the usage. In my view, Array{Int} leaves the dimension parameter unspecified, and like any optional argument it should take the most commonly expected value as its default. It’s a minor thing, but little things like this add up to make the language easier to use.


#5

I don’t necessarily agree that this would be the most commonly expected value. To me a rank-n array has n indices. If I do Array{Int}(undef) then I’ve specified the dimensions of 0 of them. Note also that you can use Vector{Int} as a constructor, but you will still need the 0.


#6

Your view is mistaken; as described in the docstring of Array,

dims may be a tuple or a series of integer arguments corresponding to the lengths in each dimension.

so Array{Int}() is provided a series of integer arguments, which just happens to be empty. This is nice and consistent with all the syntactic variations.

Arguing from expectations is always tricky, as they can be very heterogeneous depending on the programmers previous experience with other languages and language design. Enforcing consistency is much easier to implement.


#7

Array{T}(dims...) in general is a varargs constructor where the number of arguments determines the number of dimensions of the resulting array. How many arguments are there in the call Array{T}()?


#8

Maybe Vector{Int}() or more generally Array{Int,p}() should create empty arrays of the specified shape. ( the latter case is actually convenient as Array{Int,p}(zeros(Int,p)...) is rather ugly.)


#9

The {} syntax after types is usually used for type parameters, which don’t include size for an array, just ndims. So if you understand your suggestion correctly, it would be somewhat confusing.


#10

Yes I know that. I’m saying that it creates an empty array of. The specified shape: Array{T,2}() would create a 0 x 0 matrix. (Or just look at the code example I gave for the general p).


#11

I missed the use case for that, can you give an example? Eg Vector{T}() is used because I can push! into it, but I don’t think I have ever created an array with size (0, 0, ..., 0).


#12

Create an empty spot in a mutable struct to bind another array to later.


#13

Thanks for the example. I am not sure if the use case warrants a function in Base though.


#14

How is a zero-dimensional array ever useful?


#15

I think this is the point at which a consistency argument comes in though.

It never occurred to me that Array{Int, 3}() wouldn’t create an empty 3-dimensional array until just now, given I’ve used Int[] and Vector{Int}() in the past. Not having the empty constructor is a completely unnecessary gotcha given Array{Int, 1}() and it’s obvious what it’ll do [and I can imagine using it exactly as @ChrisRackauckas suggests].


#16

It throws an error because you didn’t give it 3 dimension arguments, to maintain consistency across all array constructors. The same goes for Vector{Int}(undef).

I think I agree that Array{T,N}(undef) creating rank-N arrays with all 0 dimensions would be sensible, this is of course not the same thing as Array{T}(undef), though I’m not sure I see many use cases for this. I always try pretty hard to allocate the entire array first and not push.


#17

I think I agree that Array{T,N}(undef)

There’s no need for the undef if the array is empty: just Array{T,N}() would do. Note this is how it is even in Julia v0.7-alpha for Vector:

julia> Vector{Int}()
0-element Array{Int64,1}

julia> versioninfo
versioninfo (generic function with 4 methods)

julia> versioninfo()
Julia Version 0.7.0-alpha.20
Commit c8ce43ad17 (2018-06-02 06:26 UTC)
Platform Info:
  OS: macOS (x86_64-apple-darwin17.5.0)
  CPU: Intel(R) Core(TM) i7-7700HQ CPU @ 2.80GHz
  WORD_SIZE: 64
  LIBM: libopenlibm
  LLVM: libLLVM-6.0.0 (ORCJIT, skylake)
Environment:
  JULIA_VERSION = 0.6

#18

Surprisingly, a zero dimensional array contains one entry:

julia> A = Array{Int,0}(undef)
0-dimensional Array{Int64,0}:
-1

julia> A[1] = 4
4

julia> A
0-dimensional Array{Int64,0}:
4

It’s very similar in usage to a Ref.

EDIT: This syntax makes the behaviour less bizarre looking:

julia> A[] = 3
3

julia> A
0-dimensional Array{Int64,0}:
3

#19

There is definitely a trade-off being made between “consistency” and convenience here. How you see the consistency depends upon your viewpoint. I would argue that the Vector{Int}() constructor is the odd-one out and leads to this misunderstanding.

That said, now that we have the undef constructors in 0.7, the status is slightly different. No longer is Array{Int}() the logical extension of:

Array{Int}(dim1, dim2) -> Array{Int}(dim1) -> Array{Int}() # 2 -> 1 -> 0 dimensions

Instead, we have:

Array{Int}(undef, dim1, dim2) -> Array{Int}(undef, dim1) -> Array{Int}(undef)

So, then, there remain a few questions:

  • What should Array{Int}() mean? I’d argue it should be an error — indeed it’s deprecated on 0.7 in favor of Array{Int}(undef) and will be a method error on 1.0. That seems sensible.
  • What should Array{Int, N}() mean? This is deprecated for N = 0 and is an error for all N > 1. When we last considered this, however, we didn’t have the new undef-ness. So now I suppose it’s no longer clashing with the above vararg dimensions, and could be taken to mean an “empty array.” We’d probably have to leave it as an error for N=0 since you cannot have an empty 0-dimensional array. It somewhat makes sense that you wouldn’t need to specify undef for an empty array, but I’m still not entirely convinced this is a good idea. The good news is, however, that all of this design for 1.0 is erring slightly conservative and these would all be new features that could be introduced in 1.x.

#20

It’s useful as a type stable default argument value. Assuming for a moment that Array{T, N}() would create an empty array.

function f(x::Array{T, N}, weight = Array{T, N}()) where {T, N}
    if isempty(weight)
        # Compute unweighted special case.
    else
        # Compute the general case.
    end
end

Yes, you could use nothing to indicate no weight and being a small union it would probably be fast, but full type stability is still an attractive property.