Reuse declarations

I want to declare two structures, one of which contains many of the same elements of the other. In a simple example, it would be, for example:

struct A
   a :: Int64
end
struct B
  a :: Int64
  b :: Float64
end

What I am aiming is to be able to write something like (the following example does not work, but probably makes the point):

DataA = "a :: Int64"
struct A
  eval(DataA)
end
struct B
  eval(DataA)
  b :: Int64
end

Of course, this would be used in a situation where the DataA is a long list of properties.

The same sort of construct could be used to define two structures with the same data structure, one mutable and the other not, for instance:

DataA = "a :: Int64"
struct A
  eval(DataA)
end
mutable struct MutA
  eval(DataA)
end

Actually I would like to do that to organize some data reading code, by reading the data first to a mutable structure and then passing it to an unmutable one.

I searched the docs for metaprograming options, but I could not find out how to make that work.

Thanks for the thoughts.
Leandro.

DataA = :(a :: Int64)

macro paste(ex)
    ex = eval(ex)
    :($(esc(ex)))
end

struct A
  @paste DataA
end
struct B
  @paste DataA
  b :: Int64
end
3 Likes

Probably easier to just do:

DataA = :(a::Int64)
@eval struct A
    $DataA
end

rather than defining a @paste macro.

2 Likes

Thank, both options are great.

What about initializing the immutable structure with the data from the mutable one?
For instance, something like:

DataA = quote
  x 
  y
end
@eval struct A
  $DataA
end
@eval struct MutA
   $DataA
end
MutA() = MutA(0,0)  # wandering if there is a better way to initialize these values as well
z = MutA()
z.a = 1
z.b = 2
w = A(expand(z)) # something like this

Where the expand function returns the list of values of z. For instance, this function does that, but it returns a string and thus the definition of w fails:

function expand(a)
  nfields = length(fieldnames(typeof(a)))
  ifield = 0
  list = ""
  for field in fieldnames(typeof(a))
    ifield = ifield + 1
    if ifield < nfields
      list = "$list$(@eval a.$field),"
    else
      list = "$list$(@eval a.$field)"
    end
  end
  return list
end

I understand that I could build explicitly a constructor, such as

A( x :: MutA ) = A( x.a, x.b )

but again, for structures with lots of fields that might become cumbersome.

Any idea? Thanks.

As a general note, avoid using strings for metaprogramming. It really isn’t the right tool in a language that has first class support for constructing expressions.

If you just want to shove the fields from one type into another (and performance is not crucial) in the same order you can do something like

julia> A(getfield.((z,), 1:nfields(z))...)
A(1, 2)

But having two copies of a struct, with one being mutable seems pretty odd.

2 Likes

Great, thanks for the help.

The idea of having a mutable structure with the same data structure as the immutable one is that I need to read a lot of different parameters from a file, and then create an immutable structure with all that data. Thus, since the immutable structure has to be generated at once,

x = A(1,2,"abc", ... )

I need to create temporary variables for all the fields anyway, read all these temporary variables and, at the end, create the immutable x.

What I was thinking was to use a mutable structure with the same structure of the immutable one to read clearly and beautifully all the data, using something as:

y = MutA()
y.a = data[1]
y.b = parse(Float64,somestring)

to finally initalize the immutable x variable, which will be used in performance critical code, using, as you suggest,

x = A(getfield.((y,),1:nfields(y))...)

Perhaps I am missing something trivial about immutable constructors which would turn out the task much easier?

I don’t see why this is a problem?

For example, if you read it as a Dict you could use Parameters.jl as

julia> d = Dict(:mass => 2, :velocity => 10, :name => "foo")
Dict{Symbol,Any} with 3 entries:
  :name     => "foo"
  :mass     => 2
  :velocity => 10

julia> using Parameters

julia> @with_kw struct MyParameters
           name
           mass
           velocity
       end
MyParameters

julia> MyParameters(;d...)
MyParameters
  name: String "foo"
  mass: Int64 2
  velocity: Int64 10

Or if you just want to splat a vector

julia> v = ["foo", 2, 10];

julia> MyParameters(v...)
MyParameters
  name: String "foo"
  mass: Int64 2
  velocity: Int64 10

I don’t ever see a need to use two different structs with the same fields.

2 Likes

To be truth I didn’t know the existence of the “…” syntax to expand the elements of a list in to a function call.

That is great, thank you.

Just to finish: I actually like the result obtained using the two structures, one mutable and the other not. Of course, it is only about how easy is to understand the code afterwards. But one example of the kind of thing I am aiming to use is:

AtomData = quote
  index :: Int64
  residue :: String
  name :: String
  x :: Float64
  y :: Float64
  z :: Float64
  b :: Float64
  occup :: Float64
end

@eval mutable struct ReadAtom
  $AtomData
end
ReadAtom() = ReadAtom(0,"X","X",0.,0.,0.,0.,0.)

@eval struct Atom
  $AtomData
end
Atom( atom :: ReadAtom ) = Atom([ getfield(atom,i) for i in 1:nfields(atom) ]...)

The only think I would like to improve here is the constructor for the mutable structure without parameters, which I still have to modify if I add an additional parameter to the AtomData list. Ideally I would like to provide default parameters already there.

I don’t see the use of the mutable struct at all here. You could just have directly defined

julia> Atom() = Atom(0,"X","X",0.,0.,0.,0.,0.)
Atom

julia> Atom()
Atom(0, "X", "X", 0.0, 0.0, 0.0, 0.0, 0.0)

and used that?

To set default values, Parameters.jl is good again:

julia> using Parameters

julia> @with_kw struct Atom
           index::Int64 = 0
           residue :: String = "X"
           name :: String = "X"
           x :: Float64 = 0
           y :: Float64 = 0
           z :: Float64 = 0
           b :: Float64 = 0
           occup :: Float64 = 0
       end
Atom

julia> Atom()
Atom
  index: Int64 0
  residue: String "X"
  name: String "X"
  x: Float64 0.0
  y: Float64 0.0
  z: Float64 0.0
  b: Float64 0.0
  occup: Float64 0.0
2 Likes

The use of Parameters to initialize the values is what I want indeed.

However, using simply Atom() would not allow me to actually fill this variable
with the actual data (edit: using the names of the fields), which will be read afterwards from input files, since the Atom
structure is immutable.

But I completely understand that I could replace the mutable structure with a simple
list, or with a Dict. The choice relies only on how the code looks like. I like the idea of using

x = ReadAtom() # x is muttable and will be used for data reading
x.index = 1
x.name = "Carbon"

y = Atom(x) # y is immutable and will be used in critical code

Thank you very much for all your help. I learnt a lot of things.

EDIT:

An alternative way to initialize with default parameters a muttable struct, which is convenient in my case, is this:

function initdata(X :: DataType)
  list = []
  for type in X.types
    if type == Int64
      push!(list,0)
    end
    if type == String
      push!(list,"X")
    end
    if type == Float64
      push!(list,0.)
    end
  end
  y = X(list...)
  return y
end

with which the actual code would look like:

x = initdata(ReadAtom) # muttable with default parameters
x.index = 1
x.name = "Carbon"

y = Atom(x) # immutable