Initializing lots of matrices in the same way

The code I’m working with involves a few dozen matrices, let’s call them a, b, etc. They all end up being used in quite different fashions, so it makes sense to keep them separate rather than packing them all into one large array. However, they are all matrices of the same size and are all initialized in the same way. Is there any cleaner way to do this than just a long list of initializations:

a = zeros(Float64, 10, 10)
b = zeros(Float64, 10, 10)
...
y = zeros(Float64, 10, 10)
z = zeros(Float64, 10, 10)

m = [zeros( 10, 10) for _ in 1:26] and then you can refer to them by index. E.g. instead of a it would be m[1], etc.

1 Like

I would like to keep the arrays as separate variables; the actual names are more verbose and substantially increases readability. However, your solution should work if I just do a, b, .... z = [zeros( 10, 10) for _ in 1:26] - thanks!

1 Like

You could try using a generator instead of an array comprehension:

a, b, .... z = (zeros( 10, 10) for _ in 1:26) 

Then you avoid allocating the outer array.

Generators are lazy, so I think you could even make it infinitely long, and it will still only create the same number of matrices as the number of variables on the left hand side. In other words, you don’t need to count the variables, which I somehow find more satisfying.

9 Likes

Never new that existed, thanks!

Another option is to use NamedTuples. This keeps the readable names, but allows access through both names and indices and perhaps more importantly, passing and saving of the tuple of matrices. In code:

julia> names = (:a, :b, :c)
(:a, :b, :c)

julia> length(names)
3

julia> m = NamedTuple{names}(ntuple(i->zeros(3,3),length(names)))
(a = [0.0 0.0 0.0; 0.0 0.0 0.0; 0.0 0.0 0.0], b = [0.0 0.0 0.0; 0.0 0.0 0.0; 0.0 0.0 0.0], c = [0.0 0.0 0.0; 0.0 0.0 0.0; 0.0 0.0 0.0])

julia> m.a
3×3 Matrix{Float64}:
 0.0  0.0  0.0
 0.0  0.0  0.0
 0.0  0.0  0.0

julia> m[2]
3×3 Matrix{Float64}:
 0.0  0.0  0.0
 0.0  0.0  0.0
 0.0  0.0  0.0

If the matrices are of known sizes, then consider also using SArrays or MArrays which for smaller matrices provide more optimization (see StaticArrays package).

Your comment brings me back to this other (structurally similar, it seems to me) discussion.

here

I would think that the macros used to slurp part of a NamedTuple could be adapted to this situation, where rest=() and on the right hand side is a tuple with n repeating values.
I’d be curious to see what this macro would look like?

Speaking of Struct-utally similar questions. Would a struct be better than a NamedTuple in a general situation such as this? Is there a go-to package for conversions/initialization between NamedTuples and structs? As they seem similar to me in many ways.

I don’t know why the ntuple version didn’t work for me before, so I tried to do the NamedTuple one (*)

macro assigNT(ex)
    vars = ex.args[1].args
    val = ex.args[2]
esc(quote
    $(vars...),= NamedTuple{Tuple($vars)}(ntuple(i->$val,length($vars)))
    end)
end

macro assignt(ex)
    vars = ex.args[1].args
    val = ex.args[2]
esc(quote
    $(vars...),= ntuple(i->$val,length($vars))
    end)
end


@assignt a,b,c,d = zeros(5,5)

julia> a
5×5 Matrix{Float64}:
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0

julia> c
5×5 Matrix{Float64}:
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0
 0.0  0.0  0.0  0.0  0.0

(*)

$(vars...),= ntuple(i->$val,length($vars))

maybe I overlooked the ‘,’ before the ='.
Can anyone explain the difference with and without.
I guess it’s something analogous to the fact that (1,) is a tuple of only the value 1, while (1) is only the value 1.
But perhaps in the context of the expressions there is something else

I found this way to render in the form of a macro to the solution via generator, but I’m sure there is a simpler and faster way to achieve the same result via macro.

macro assigngen(ex)
    vars = ex.args[1].args
    val = ex.args[2]
    n=length(vars)
    itr = Expr(
            :(=),
            :_,
            esc(:(1:($n)))
        )
        
gen= Expr(
    :generator,
    esc(:($val)),
    itr
)
Expr(
    :(=),
    esc(:($(vars...),)),
    gen
)
end

Why are you using a macro instead of a function? Here’s a macroless solution:

f(::Val{names}, size::NTuple{n, Int}) where {names, n} =
  NamedTuple{names}(ntuple(
    let s = size
      _ -> zeros(s...)
    end,
    Val{length(names)}(),
  ))

In the REPL:

julia> f(Val((:x, :y, :z)), (2, 3))
(x = [0.0 0.0 0.0; 0.0 0.0 0.0], y = [0.0 0.0 0.0; 0.0 0.0 0.0], z = [0.0 0.0 0.0; 0.0 0.0 0.0])

FYI, macros should be avoided whenever possible.

Some solutions with functions had already been proposed and I wanted to try, for my exercise, to make some macros that after a lot of effort, I managed to get out.
On the merits of the question, the expression that best responds to the OP’s request, in my opinion, is the one @DNF’s with the generator.
As for macros, I’ve read that anything a macro does can be done by some function, so strictly speaking macros are never needed.
But in some cases they can be convenient.

In this case, @DNF’s solution seems to be more functional and convenient than a macro.

The solution with a generator seems like it could be more problematic, or at least less flexible.

The problem is that the length of the generator is a run-time value, while the number of your matrices is akin to the length of a Tuple, which is a compilation-time value. The mismatch might not matter in a lot of cases due to Julia’s smart type inference and constant propagation, however it seems like it could cause run time dispatch and slow things down in some cases if used carelessly.

1 Like

You can make the generator infinitely long, it doesn’t matter. It’s the lhs that determines the length.

For the fun of it (probably not the most elegant way of doing that):

julia> struct InfinitelyIterable end

julia> import Base:iterate

julia> Base.iterate(::InfinitelyIterable, state) = ((InfinitelyIterable(), 1), 1)

julia> a, b, c = (zeros(3,3) for _ in InfinitelyIterable());

julia> a
3×3 Matrix{Float64}:
 0.0  0.0  0.0
 0.0  0.0  0.0
 0.0  0.0  0.0

julia> b
3×3 Matrix{Float64}:
 0.0  0.0  0.0
 0.0  0.0  0.0
 0.0  0.0  0.0

julia> c
3×3 Matrix{Float64}:
 0.0  0.0  0.0
 0.0  0.0  0.0
 0.0  0.0  0.0
1 Like

Indeed. You can also use

a, b, c = (zeros(3,3) for _ in Iterators.countfrom());
1 Like

without the use of an intermediate generator

julia> A,B,C,D =Iterators.repeated(ones(2,3))
Base.Iterators.Repeated{Matrix{Float64}}([1.0 1.0 1.0; 1.0 1.0 1.0])

julia> A
2×3 Matrix{Float64}:
 1.0  1.0  1.0
 1.0  1.0  1.0

julia> D
2×3 Matrix{Float64}:
 1.0  1.0  1.0
 1.0  1.0  1.0


julia> A,B,C,D =Iterators.repeated(ones(2,3))
Base.Iterators.Repeated{Matrix{Float64}}([1.0 1.0 1.0; 1.0 1.0 1.0])

julia> A[1] = 9
9

julia> B
2×3 Matrix{Float64}:
 9.0  1.0  1.0
 1.0  1.0  1.0
3 Likes
julia> import IterTools as Itr

julia> A,B,C,D = Itr.repeatedly(()->ones(2,3))

julia> A[1] = 9

julia> B
2×3 Matrix{Float64}:
 1.0  1.0  1.0
 1.0  1.0  1.0
1 Like

Another option is to use @eval:

vars = (:a, :b, :c)
for var in vars
    @eval $var = zeros(2, 2)
end