Back to the basics: array literals in Julia
Introduction
Array literals are one of the basic constructs in the Julia language that essentially every developer learns during the first session. However, the exact mechanism of how these literals work are complex if one wants to understand them in full.
In this post I want to give several examples how the array literals in Julia work and highlight upcoming changes in the Julia language that might affect your code, if you happen to use them.
Expect that the material I present today is more advanced than usual, but I think the topics I cover are relevant for any Julia developer writing production code.
This post was written using Julia 1.7.0 and Julia 1.9.0-DEV.245.
The rules
In the Julia Manual section on array literals you can read that:
Arrays can also be directly constructed with square braces; the syntax
[A, B, C, ...]
creates a one dimensional array (i.e., a vector) containing the comma-separated arguments as its elements. The element type (eltype
) of the resulting array is automatically determined by the types of the arguments inside the braces. If all the arguments are the same type, then that is itseltype
. If they all have a common promotion type then they get converted to that type using convert and that type is the array’seltype
. Otherwise, a heterogeneous array that can hold anything — aVector{Any}
— is constructed; this includes the literal[]
where no arguments are given.
In short this rule means that:
- when you write
[A, B, C]
Julia checks types ofA
,B
, andC
; - if these types are equal then a
Vector
having this type is created; - if these types are not equal but can be promoted to a common type then this common type is used as element type of an array;
- otherwise an
Any
element type is used.
Let us see these rules in action.
The first case is when all passed elements have the same element type:
julia> [1, 2, 3]
3-element Vector{Int64}:
1
2
3
Here we passed three integers, so element type of created vector is Int64
.
Now let us see a second rule at work. We pass some values that have a common promotion type:
julia> [true, 2.0, 3]
3-element Vector{Float64}:
1.0
2.0
3.0
We have passed a Bool
, Float64
, and Int64
value and they all got converted
to Float64
, because it is their common promotion type.
Sometimes the result can have a type that is not even any of the types of passed elements:
julia> [big(1), 2.0, 3.0]
3-element Vector{BigFloat}:
1.0
2.0
3.0
This time we get a conversion to BigFloat
as this is a common promotion type
of BigInt
and Float64
.
Finally sometimes a common promotion type is not concrete in which case no conversion takes place:
julia> [[1, 2], ["a", "b"]]
2-element Vector{Vector}:
[1, 2]
["a", "b"]
In this case Vector{Int64}
and Vector{String}
have a common promotion type
Vector
which is UnionAll
so no conversion of elements occurred.
The last case in our set of rules was that there is no common promotion type
for the elements of created vector. In this case the result has Any
element
type:
julia> [1, "2", '3']
3-element Vector{Any}:
1
"2"
'3': ASCII/Unicode U+0033 (category Nd: Number, decimal digit)
In this example Int64
, String
, and Char
do not have a common promotion
type.
Upcoming changes in how array literals work
The codes given above work the same way under Julia 1.7 and Julia nightly. Now we are getting to a territory when differences will be present.
Start a Julia 1.7 session in your terminal and run the following code:
julia> [1:2, [1, 2]]
2-element Vector{AbstractVector{Int64}}:
1:2
[1, 2]
julia> [1:2, ["a", "b"]]
2-element Vector{AbstractVector}:
1:2
["a", "b"]
As you can see the resulting array has an abstract element type.
In particular no conversion of the elements of the array literal happened.
The reason of this behavior is the return value of the promote_type
function:
julia> promote_type(typeof(1:2), typeof([1, 2]))
AbstractVector{Int64} (alias for AbstractArray{Int64, 1})
julia> promote_type(typeof(1:2), typeof(["a", "b"]))
AbstractVector (alias for AbstractArray{T, 1} where T)
As you can see the result of the pomote_type
is used as an element type of the
created vectors.
Now switch to current Julia nightly build (I used Julia 1.9.0-DEV.245) and run the same code:
julia> [1:2, [1, 2]]
2-element Vector{Vector{Int64}}:
[1, 2]
[1, 2]
julia> [1:2, ["a", "b"]]
2-element Vector{Vector{Any}}:
[1, 2]
["a", "b"]
As you can see the result is different. Now elements of array literals are
converted to concrete Vector{Int64}
and Vector{Any}
types respectively.
Let us check the result of promote_type
:
julia> promote_type(typeof(1:2), typeof([1, 2]))
Vector{Int64} (alias for Array{Int64, 1})
julia> promote_type(typeof(1:2), typeof(["a", "b"]))
Vector{Any} (alias for Array{Any, 1})
It looks that promotion rules have changed since Julia 1.7. Indeed they were
updated in this PR. The PR introduces a rule that states that if no
promote_rule
is defined for two arrays then typejoin
on their eltype
s is run
instead and an Array
having this element type as the container is returned.
Here is the crucial line of code that was added in this PR:
promote_result(::Type{<:AbstractArray{T,n}}, ::Type{<:AbstractArray{S,n}},
::Type{Bottom}, ::Type{Bottom}) where {T,S,n} =
(@inline; Array{promote_type(T,S),n})
However, let me recall you the earlier example we have given (run on Julia nightly):
julia> [[1, 2], ["a", "b"]]
2-element Vector{Vector}:
[1, 2]
["a", "b"]
Since this time a common promotion type exists, and is Vector
no conversion is done and the resulting container does not have a concrete
element type (as opposed to e.g. [1:2, ["a", "b"]]
shown above).
Why is this relevant? As you can see, when you migrate from Julia 1.7 to any Julia release of Julia that has this PR incorporated you must be aware that your old code might stop working the same way it did if it used array literals that contained arrays inside.
Conclusions
There is one key takeaway from my examples.
If in your current code you use array literals to store arrays inside them please review them before upgrading to latest Julia.
In particular if you do not want an implicit conversion use an element type prefix in front of the array literal for example like this:
julia> Any[[1, 2], ["a", "b"]]
2-element Vector{Any}:
[1, 2]
["a", "b"]
julia> AbstractVector[1:2, [1, 2]]
2-element Vector{AbstractVector}:
1:2
[1, 2]
Passing an explicit element type prefix is a practice that I have currently adopted in all of my production codes as this is the safest way to make sure my programs run exactly as I want.