Search code examples
structjuliaboilerplate

Julia: how to avoid boilerplate code when structs sharing attributes


It happens quite regularly that different structs (EDIT: types) should share some attributes. If i got it right as a beginner: In Julia, you can extend abtract types, but they may not have any attributes. Concrete types (=structs) are not extendable. So, is there a way to avoid code repetition (for attributes name and weight) like in the given example?

abstract type GameObj end

struct Gem <: GameObj
  name::String
  weight::Int64
  worth::Int64
end

struct Medicine <: GameObj
  name::String
  weight::Int64
  healing_power::Int64
end

g = Gem("diamond", 13, 23000)
m = Medicine("cough syrup", 37, 222)

I tried to put the shared attributes into an extra struct, like in the following example. Advantage: No code repetition. Disadvantages: calling constructors and getting attributes (g.attributes.weight) is inconvenient.

abstract type GameObj end

struct GameObjAttr
  name::String
  weight::Int64
end

struct Gem <: GameObj
  attributes::GameObjAttr
  worth::Int64
end

struct Medicine <: GameObj
  attritbutes::GameObjAttr
  healing_power::Int64
end

g = Gem(GameObjAttr("diamond", 13), 23000)
m = Medicine(GameObjAttr("cough syrup", 37), 222)

The third example uses inner constructors, now the constructor calls are more easy to read and write, but now we have some code repetition in the inner constructors. Plus: Getting the shared attributes is still inconvenient:

abstract type GameObj end

struct GameObjAttr
  name::String
  weight::Int64
end

struct Gem <: GameObj
  attributes::GameObjAttr
  worth::Int64
  Gem(name::String, weight::Int64, worth::Int64) = new(GameObjAttr(name, weight), worth)
end

struct Medicine <: GameObj
  attributes::GameObjAttr
  healing_power::Int64
  Medicine(name::String, weight::Int64, healing_power::Int64) = new(GameObjAttr(name, weight), healing_power)
end

g = Gem("diamond", 13, 23000)
m = Medicine("cough syrup", 37, 222)

Is there another, better way to avoid this kind of code repetition? (Besides that: is it necessary to declare types inside the inner constructor, or can we leave that?)

Thanks in advance.


Solution

  • You can use Julia's metaprogramming abilities for this.

    abstract type GameObj end
    
    type_fields = Dict(
                       :Gem => (:worth, Int64),
                       :Medicine => (:healing_power, Int64)
                      )
    
    
    for name in keys(type_fields)
      @eval(
        struct $name <: GameObj
          name::String
          weight::Int64
          $(type_fields[name][1])::$(type_fields[name][2])
        end
      )
    end
    
    g = Gem("diamond", 13, 23000)
    m = Medicine("cough syrup", 37, 222)
    

    This is similiar to you copy-pasting the code but it allows you to do it programmatically. Note that we use $ to interpolate external values into the expression which is being executed in the loop.

    Edit (based on question in comments):

    If you want to be able to add an arbitrary number of fields for the different types you can make a minor modification to the above code:

    abstract type GameObj end
    
    type_fields = Dict(
                       :Gem => ((:worth, Int64),
                                (:something_else, Any)),
                       :Medicine => ((:healing_power, Int64),)
                      )
    
    
    for name in keys(type_fields)
      @eval(
        struct $name <: GameObj
          name::String
          weight::Int64
          $(map( x -> :($(x[1])::$(x[2])), type_fields[name])...)
        end
      )
    end
    
    g = Gem("diamond", 13, 23000, :hello)
    m = Medicine("cough syrup", 37, 222)