What on earth does Any[...] do?

I’ve been using Julia nearly daily for about a year and thought I was fluent in the basics by now. Here’s an example that proves I was mistaken.

(I believe that) The syntax T[] defines an array literal and casts the elements to type T. For example:

julia> Float64[1,2,3]
3-element Array{Float64,1}:
 1.0
 2.0
 3.0

Similarly, (I believe that) Any[] is useful for initializing an array with elements of type Any, so that it can hold elements of other types later.

julia> Any[1,2,3]
3-element Array{Any,1}:
 1
 2
 3

So far so good. Now let’s do the same thing on a more complex array:

julia> x = Any[ [1,2,3], ["1","2","3"], [1.0,2.0,3.0] ]
3-element Array{Any,1}:
 [1, 2, 3]
 String["1", "2", "3"]
 [1.0, 2.0, 3.0]

julia> typeof.(x)
3-element Array{DataType,1}:
 Array{Int64,1}
 Array{String,1}
 Array{Float64,1}

Instead of casting the elements of x to Any, it seems to have done the complete opposite and made the types more specific. On a heterogeneous array, I would have expected Any[] to produce output similar to what you get if you leave out the Any specification, like this:

julia> y = [ [1,2,3], ["1","2","3"], [1.0,2.0,3.0] ]
3-element Array{Array{Any,1},1}:
 Any[1, 2, 3]
 Any["1", "2", "3"]
 Any[1.0, 2.0, 3.0]

What in the world is going on here? What is this called, and where is it documented? (Googling “Julia Any” isn’t very helpful …)

julia> typeof(convert(Any, 1))
Int64

julia> typeof(convert(Any, "foo"))
String

in fact

convert(::Type{Any}, x) = x
1 Like

Any is an abstract type, so you can’t have concrete instantiations of it. However, containers can have Any as their element type. When this is true it is required that the elements of the array have types <: Any, but since Any is an abstract type (and all concrete types are subtypes of it), convert(Any, x) = x, \forall x.

When you do x = Any[ [1,2,3], ["1","2","3"], [1.0,2.0,3.0]] all you are doing is declaring that your Array should have eltype Any. Since Vector{Int64} <: Any, Vector{String} <: Any and Vector{Float64} <: Any, this constructor is perfectly valid and the conversions that take place are trivial.

It’s a little less clear to me why the result is different when you omit the prefixed Any, but it’s just a result of how the compiler processes literals.

2 Likes

I think the correct question is what does [[x1, y1, z1], [x2, y2, z2], [x3, y3, z3]] do instead of Any[...] do.
[[x1, y1, z1], [x2, y2, z2], [x3, y3, z3]] will try to cast all the elements into a common type, and in your example, the common type if Any, so it is the behaviour of

julia> y = [ [1,2,3], ["1","2","3"], [1.0,2.0,3.0] ]
3-element Array{Array{Any,1},1}:
 Any[1, 2, 3]
 Any["1", "2", "3"]
 Any[1.0, 2.0, 3.0]
1 Like

Interesting! This is a bit subtle, but it’s a natural result of arrays attempting to promote their contents to a common type.

julia> Any[ [1,2,3], ["1","2","3"], [1.0,2.0,3.0] ]
3-element Array{Any,1}:
 [1, 2, 3]
 String["1", "2", "3"]
 [1.0, 2.0, 3.0]

julia> [ [1,2,3], ["1","2","3"], [1.0,2.0,3.0] ]
3-element Array{Array{Any,1},1}:
 Any[1, 2, 3]
 Any["1", "2", "3"]
 Any[1.0, 2.0, 3.0]

In the first case, the outer array has explicit element type Any so its elements are taken as is when constructing the array. In the second case, the outer array has no explicit element type so typejoin is called to try to find a “reasonable” common element type – which is Vector{Any} since all of the things passed to it are vectors but they don’t have a common element type. So they’re all converted to the type Vector{Any} before construction.

The thing that’s questionable here is whether arrays should recursively promote to a common type like this when it ends up “pessimizing” their individual element types so much. The motivation for the array promotion rule is (more common) cases like this:

julia> [ [1, 2, 3], [1, 0.5, 0.25, 0.125], [-1, -2] ]
3-element Array{Array{Float64,1},1}:
 [1.0, 2.0, 3.0]
 [1.0, 0.5, 0.25, 0.125]
 [-1.0, -2.0]

Here’s it’s much better in terms of types and performance to convert all of the internal arrays to a single common concrete element type. I’ve filed an issue about this: #24988 since it’s worth considering changing the array promotion rule here.

3 Likes

BTW, T[] will only cast the elements to type T. In the case of vector of vectors, you need to do something like Vector{T}[] to convert the elements in the inner vectors, for example, Vector{Any}[ [1,2,3], ["1","2","3"], [1.0,2.0,3.0] ] will cast all the elements in the inner vectors to type Any.

1 Like

This is off topic, but can I ask about this
convert(::Type{Any}, x) = x
syntax?

Specifically the ::Type{Any} part. How is that understood by convert in this example?

Does ::Type{Any} refer to the type itself? Or an instance of the type?

I see that I can do

function f1(::Array{Int64})
   return true # returns true as long as the argument is <: Array{Int64}, I think?
end 

but not

function f2(Array{Int64})

which is a syntax error.

I guess I could use ::Type{Any} to test that whatever is passed to f1 is an instance of the type. Are there other uses for this syntax? Is there some syntax that would let me do:

function f3(::Array{Int64})
   return size( the argument ) # but how do I refer to that in this scope?
end

I see this syntax around sometimes but I don’t think I fully get it.

For any Julia type T, we have T::Type{T}. This lets you write functions that dispatch on a particular type passed in as an argument:

julia> f(::Type{<:AbstractFloat}) = "a type which is float-like"
f (generic function with 2 methods)

julia> f(::Type{Int}) = "the type Int"
f (generic function with 3 methods)

julia> f(::Type{String}) = "the type String"
f (generic function with 4 methods)

julia> f(Float64)
"a type which is float-like"

julia> f(Int)
"the type Int"

julia> f(String)
"the type String"

this should actually look weird, because it’s kind of like we’re dispatching on the value of the argument rather than its type. After all, Int, Float64 and String are all just values of type DataType:

julia> typeof(Int)
DataType

But because dispatching on types as function arguments is so useful, the Type construct is specifically designed to make that possible. This leads to a particular weirdness:

julia> Int::Type{Int}
Int64

julia> Int isa Type{Int}
true

julia> typeof(Int)
DataType

julia> typeof(Int) == Type{Int}
false

fortunately, that’s the only weirdness. Otherwise, Type arguments are just like any other function argument; they just look weird because they’re often written as f(::Type{T}) with the variable name omitted rather than f(x::Type{T}). Including the variable name makes the behavior more clear:

julia> function f(x::Type{T}) where T
         @show x
         @show T
         @assert x == T
       end
f (generic function with 1 method)

julia> f(Int)
x = Int64
T = Int64

julia> f(Real)
x = Real
T = Real

julia> f(Float64)
x = Float64
T = Float64

You can see in this case why the variable name x is typically omitted, because its value (the type that was passed to f) is already captured in the variable T. So we would generally write f as:

julia> function f(::Type{T}) where T
         @show T
       end
f (generic function with 1 method)

julia> f(Int)
T = Int64
Int64

julia> f(Real)
T = Real
Real

and refer to the type as T inside the function.

The most common place to see Type{T} is in convert() methods:

convert(::Type{MySpecialType}, x::Int) = make_my_special_type_from_int(x)
convert(::Type{Int}, y::MySpecialType) = make_int_from_my_special_type(y)

since convert is called with the destination type as its first argument:

y = convert(MySpecialType, 1)
convert(Int, y)

Regarding your last question:

function f3(::Array{Int64})
return size( the argument ) # but how do I refer to that in this scope?
end

dispatching with Type{T} is useful when you expect a type as an argument, not an instance of that type. So your f3 would just be:

function f3(x::Array{Int64})
  return size(x)
end

just like any other method in Julia.

8 Likes

Update: so, I ran those examples in a 0.6 session without realizing it. This is already changed/fixed on master:

julia> VERSION
v"0.7.0-DEV.2976"

julia> [ [1, 2, 3], ["1", "2", "3"], [1.0, 2.0, 3.0] ]
3-element Array{Array{T,1} where T,1}:
 [1, 2, 3]
 ["1", "2", "3"]
 [1.0, 2.0, 3.0]
1 Like