Immutable composite type?

I have a composite type with many fields. The instance of it is essentially unchanged after initialization. So it could be declared as a immutable struct. But since it has many fields, I currently declared it as a mutable struct so that I use a constructor that looks more elegant like:

function MyStruct(some values, etc)
    this = new()
    this.field1 = such such
    this.field2 = such such
    ....
    this.fieldN = such such
    this
end

If I make it immutable, the way to code the constructor as I know would be much less elegant (or is it not?), like this:

function MyStruct(some values, etc)
    field1 = such such
    field2 = such such
    ....
    fieldN = such such
    new(field1, field2, ..., fieldN)
end

The “new” statement can be quite messy with strict care of ordering of arguments and spanning to many lines.

So is there better way to define an immutable type? Or, is it worthwhile to make such large composite type immutable?

Check out https://github.com/mauro3/Parameters.jl

4 Likes

Parameters seems to only deal with primitive types. There are arrays and strings in my structs.

I don’t think thats true?

This file includes: vectors, dicts, SymEngine symbols, etc.

Thanks for pointing that out and showing an example of using Parameters. But since I don’t have default values, what benefits does Parameters provide over regular mutable struct?

Does Parameters.jl still have a performance penalty in Julia 0.7?

@pmarg, please, I am not convinced on using Parameters. Can you post your question elsewhere?

The new code has a close number of characters to the other code. One problem in both codes however is replicating the word field or replicating the word this. This problem can be overcome with Paramters.jl and a twist.

If you define your such such in functions, then you can have a somewhat elegant code that looks like this:

julia> using Parameters

julia> f1() = 1; f2() = 2;

julia> @with_kw struct MyType
           a::Int = f1()
           b::Int = f2()
       end
MyType

julia> MyType(1, 2)
MyType
  a: Int64 1
  b: Int64 2

julia> MyType()
MyType
  a: Int64 1
  b: Int64 2

julia> MyType(a = 1, b = 2)
MyType
  a: Int64 1
  b: Int64 2

Note that you have access to the keyword constructor and the positional argument constructor. Also the default values are optional.

Given the above keyword constructor you can then define another external constructor that looks more like the function you want:

function MyStruct(some values, etc)
    MyStruct(field1 = such such, 
             field2 = such such, 
    	     fieldN = such such)
end

If such such is small, this will look elegant, otherwise your original attempt will probably be more elegant. Note that you have more flexibility now in wrapping such such in functions or macros since we are outside the type definition, so you can pass on some values and etc to another function to do a computation and come back with the result. Or you can contract some code in such such in a macro as such:

macro suchsuch1
	return esc(quote
	        such such
		such such
	end)
end

and the same thing for the macros suchsuch2 and suchsuch3. Then at call site just use:

function MyStruct(some values, etc)
    MyStruct(field1 = @suchsuch1, 
	     field2 = @suchsuch2, 
             fieldN = @suchsuch3)
end

The macro approach means that you don’t have to pass any arguments.

The same macro approach can be used with the positional argument constructor or perhaps even simpler, straight into your inner constructor. This is probably the solution you were looking for, but since I wrote all the above anyways, I am commenting the whole thing anyways!

1 Like

Generally, I try to avoid composite types with many (say 7+) fields. I don’t know the specifics of your problem, but perhaps you can try to organize them into smaller composite types, or use fields of type SVector (if some of them have a natural integer indexing), NamedTuple, etc.

This is not a personal question because I don’t use or consider using Parameters.jl. But since someone suggested this package, I thought it would be helpful to discuss performance issues because it might not be appropriate for your problem.

While everyone is talking about Parameters.jl I may as well add that I wrote this recently:

It’s kind of a minimalist version of Parameters.jl, with easy override of field values and the ability to add default constructors to 3rd party structs. It’s 46 lines, with a single 180 line dependency. But not in the registry yet, and probably not for ages at the rate I’m actually registering my packages right now…

We should not derail this topic, so I give you a brief answer. If that this not sufficient, then you should open a new topic (and ping me). As of Julia 0.7, keyword arguments are fast, although not quite as fast as normal ones and this shows in Parameters.jl:

julia> using Parameters, BenchmarkTools                                           
                                                                                  
julia> @with_kw struct B; a::Int = 1; b::Float64 = 1; end
B

julia> @btime B();     # as fast as possible                                                          
  1.972 ns (0 allocations: 0 bytes)                                               
                                                                                  
julia> @btime B(a=2);        # slight hit                                                     
  4.477 ns (0 allocations: 0 bytes)                                               
                                                                                  
julia> @btime B(2,3);      # positional is also as fast as possible                                                       
  1.965 ns (0 allocations: 0 bytes)                                               
1 Like

@Tamas_Papp, can you please explain about what you were saying?

I am not sure which part is unclear.

Why you would avoid composite type in preference of SVector or NameTuple? And what are the problems of composite types with many fields?

I would not avoid a composite type, but combine it with the latter two. Eg instead of

struct BigProblem
    α
    β
    γ
    κ₁
    κ₂
end

#vs

struct BigProblem
    greeks::NamedTuple{(α, β, γ)}
    κs::SVector{2} # in a real life problem I would parametrize <:Real, too
end

Of course the above example is inane, one would need to know about the problem domain to organize things conceptually.

1 Like

What problem are you solving by doing this?

As I said above, having too many fields in a struct. I think of that as code smell.

2 Likes

Thanks for answering.

But what’s in your mind that says many fields in a composite type pose a problem?

I consider it as a code smell too. It usually leads to less generic and more cumbersome and buggy code.
For instance

struct Particle
   x
   y
   z
   u
   v
   w
end

vs

struct Particle{N,T}
    position::SVector{N,T}
    velocity::SVector{N,T}
end

Say you want to do things like rotate and translate you particle.
With the latter definition no problem:

  • It is just vector addition/matrix multiplication
  • Works in any dimension

With the former its more work and prone to typos. Also if one day you need 2d particles, you have to duplicate all the code.

1 Like