Julia: array of arrays with different types

First, if you know that intermediate lists always have 3 elements, you'll probably be better off using Tuple types for those. And tuples can specify independently the types of their elements. So something like this might suit your purposes:

julia> l = [(Int64[], Int64[], Float64[]) for _ in 1:10]
10-element Array{Tuple{Array{Int64,1},Array{Int64,1},Array{Float64,1}},1}:
 ([], [], [])
 ([], [], [])
 ([], [], [])
 ([], [], [])
 ([], [], [])
 ([], [], [])
 ([], [], [])
 ([], [], [])
 ([], [], [])
 ([], [], [])

julia> push!(l[1][3], 5)
1-element Array{Float64,1}:
 5.0

julia> l
10-element Array{Tuple{Array{Int64,1},Array{Int64,1},Array{Float64,1}},1}:
 ([], [], [5.0])
 ([], [], [])   
 ([], [], [])   
 ([], [], [])   
 ([], [], [])   
 ([], [], [])   
 ([], [], [])   
 ([], [], [])   
 ([], [], [])   
 ([], [], [])   

A few details to note here, that might be of interest to you:

  • Empty but typed lists can be constructed using T[], where T is the element type.

  • collect(f(i) for i in 1:n) is essentially equivalent to a simple comprehension (like you're used to in python): [f(i) for i in 1:n]. Note that since variable i plays no role here, you can replace it with a _ placeholder so that it more immediately appears to the reader that you're essentially creating a collection of similar objects (but not identical, in the sense that they don't share the same underlying memory; modifying one won't affect the others).

  • I don't know of any better way to initialize such a collection and I wouldn't think that using collect(or a comprehension) is a bad idea here. For collections of identical objects, fill provides a useful shortcut, but it wouldn't apply here because all sub-lists would be linked.




Now, if all inner sublists have the same length, you might want to switch to a slightly different data structure: a vector of vectors of tuples:

julia> l2 = [Tuple{Int64,Int64,Float64}[] for _ in 1:10]
10-element Array{Array{Tuple{Int64,Int64,Float64},1},1}:
 []
 []
 []
 []
 []
 []
 []
 []
 []
 []

julia> push!(l2[2], (1,2,pi))
1-element Array{Tuple{Int64,Int64,Float64},1}:
 (1, 2, 3.141592653589793)

julia> l2
10-element Array{Array{Tuple{Int64,Int64,Float64},1},1}:
 []                         
 [(1, 2, 3.141592653589793)]
 []                         
 []                         
 []                         
 []                         
 []                         
 []                         
 []                         
 []                         

Francois has given you a great answer. I just wanted to raise one other possibility. It sounds like your data has a fairly complicated, but specific, structure. For example, the fact that your outer list has 1000 elements, and your inner list always has 3 lists...

Sometimes in these situations it can be more intuitive to just build your own type(s), and write a couple of accessor functions. That way you don't end up doing things like mylist[3][2][6] and forgetting which index refers to which dimension of your data. For example:

struct MyInnerType
    field1::Vector{Int}
    field2::Vector{Int}
    field3::Vector{Float64}
end
struct MyOuterType
    x::Vector{MyInnerType}
    function MyOuterType(x::Vector{MyInnerType})
        length(x) != 1000 && error("This vector should always have length of 1000")
        new(x)
    end
end

I'm guessing here, but perhaps accessor functions like this would be useful for, e.g. field3:

get_field3(y::MyInnerType, i::Int)::Float64 = y.field3[i]
get_field3(z::MyOuterType, iouter::Int, iinner::Int)::Float64 = get_field3(z.x[iouter], iinner)

Remember that there is no performance penalty to using your own types in Julia.

One other thing, I've included all type information in my functions above for clarity, but this is not actually necessary for getting maximum performance either.