`I can get you close to what you intended.
We use the fact that ordinary Vectors
are mutable, one may overwrite the value
currently held any of the vector’s indices.
So, the first insight is to use a Vector
to carry the (initially) empty NamedTuples.
Let’s try.
[ (;)
is how to write an empty NamedTuple ]
{copy and paste the examples into the REPL}
n_empty_namedtuples(n) = [(;) for i in 1:n]
two_empty_namedtuples = n_empty_namedtuples(2)
the second line gives the result
2-element Vector{NamedTuple{(), Tuple{}}}:
NamedTuple()
NamedTuple()
That means two_empty_namedtuples
is a
vector of 2 elements, and these elements
are each typed NamedTuple{(), Tuple{}}
.
… aside, a look at how the type is given
[while that type looks unusual, it is
just a parameterized type with two
parameters: the first parameter is
a Tuple (here, it is empty) and the
second parameter is a Tuple Type,
(here it is a Tuple Type with no elements).
see the docs about Parametric types.
Let’s look at a NamedTuple that has content:
nt = (A = 1, B = 2)
typeof_nt = typeof(nt);
fieldnames(typeof_nt) == (:A, :B)
fieldtypes(typeof_nt) == (Int64, Int64)
# the typeof() a NamedTuple is like this
#
# NamedTuple{ (fieldnames as symbols),
# Tuple{ fieldtypes } }
#
typeof_nt ==
NamedTuple{ (:A, :B),
Tuple{Int64, Int64} }
We created two_empty_namedtuples
.
Next, you want to replace the empty
NamedTuples with new NamedTuples.
[all sharing the same fieldnames and
fieldtypes … differing in their values
- all are of the same length,
- all have the same keys in the same order,
- each may assign its own values to the keys]
We can try, but this
new_namedtuple = (A = 1, B = 2)
two_empty_namedtuples[1] = new_namedtuple
generates a MethodError
(1) "Cannot `convert` an object of type
(2) NamedTuple{(:A, :B),Tuple{Int64,Int64}}
(3) to an object of type
(4) NamedTuple{(),Tuple{}}"
The typeof(new_namedtuple)
is
NamedTuple{(:A, :B),Tuple{Int64,Int64}}
and this is written on line (2).
The typeof( (;) )
is
NamedTuple{(), Tuple{}}
and this is written on line (4)
The MethodError says when we tried to
overwrite the empty NamedTuple with
the new_namedtuple, Julia looked at
each value’s type and found they were
incompatable. To use a new NamedTuple
to overwrite one already present within
a Vector, the two NamedTuples must be
of matching type.
For two NamedTuple types to match, both must
have the same length, the same fieldnames,
the same ordering of the fieldnames, and
the same fieldtypes with the same ordering.
Their values may differ, as long as the
value’s types are the same.
Well, the empty NamedTuples certainly
do not have the same lenghts as the
new NamedTuples that are to replace them.
Fortunately, NamedTuple types without
any fieldnames or fieldtypes specified
is a special flavor in the land of types:
This is different from empty NamedTuple
types, we have not constrained the
number of fields that there may be,
only insisted that information is not
present when given the “bare” type NamedTuple
(the type absent parameters,
the name for this kind of type is UnionAll
because it represents the union of all
possible legal parameterizations).
That allows us to do this
n_empty_namedtuples(n) =
NamedTuple[(;) for i in 1:n]
two_namedtuples = n_empty_namedtuples(2)
new_namedtuple = (A = 1, B = 2)
two_namedtuples[1] = new_namedtuple
two_namedtuples[1] == new_namedtuple
two_namedtuples[2] == (;)
How is the MethodError avoided?
Look at what we did:
errorful = [(;), (;)];
successful = NamedTuple[(;), (;)];
eltype(errorful) == NamedTuple{(), Tuple{}
eltype(successful) == NamedTuple
The errorful version has an eltype that
is a NamedTuple type with parameters given
[ the parameters are () and Tuple{} ].
The successful version has an eltype that
is a UnionAll NamedTuple type (no parameters).
As a UnionAll type is implicitly gathering
all possible legitimate parameterizations
(the way each parameter is given, is set),
the specific set of parameter values that
happens to belong to typeof(new_namedtuple)
is among the “all possible”, so assignment
(overwriting) works.
n_empty_namedtuples(n) =
NamedTuple[(;) for i in 1:n]
my_tups = n_empty_namedtuples(3)
ok = all( isempty.(my_tups) ) &&
length(my_tups) == howmany
for i in 1:length(my_tups)
my_tups[i] = (A = i, B = i+1)
println(my_tups[i])
end
# (A = 1, B = 2)
# (A = 2, B = 3)
# (A = 3, B = 4)
As it stands, each element of the vector my_tups remains open
to being overwritten by any other NamedTuple, compatible or not:
copyof_my_tups = deepcopy(my_tups)
incompat1 = (X = 3, Y = 2, Z = 1)
incompat2= (B = 'b', A = 'a')
copyof_my_tups[1] = incompat1
copyof_my_tups[2] = incompat2
copyof_my_tups
# 3-element Vector{NamedTuple}:
# (X = 3, Y = 2, Z = 1)
# (B = 'b', A = 'a')
# (A = 3, B = 4)
Here is a way to obtain a new vector not allowing incompatible overwrites,
typeconstraint = typeof(my_tups[1])
# NamedTuple{(:A, :B), Tuple{Int64, Int64}}
welldressed = typeconstraint[ my_tups... ]
# 3-element Vector...
# (A = 1, B = 2)
# (A = 2, B = 3)
# (A = 3, B = 4)
welldressed[1] = incompat1
# MethodError: Cannot `convert` an object of type ...