universal_amateur
universal_amateur

Reputation: 149

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.

Upvotes: 4

Views: 251

Answers (3)

Korsbo
Korsbo

Reputation: 724

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)

Upvotes: 2

universal_amateur
universal_amateur

Reputation: 149

Thank you!

Perhaps Julia's structs truly come into their own mainly when handling massed arrays of thousands of the same type.

Ok, so if i get you right, a type hierarchy as often seen in object oriented languages does not meet Julias high performance capabilities. This makes sense.

I varied your code example a little, now keys are Sympbols.

GameObject = Dict{Symbol, Any}

makegem(weight, worth) = GameObject(:name => "gem", :weight => weight, :worth => worth)
makemedicine(weight, healing_power) = GameObject(:name => "medicine", :weight => weight, :healing_power => healing_power)

addweight(o1::GameObject, o2::GameObject) = o1[:weight] + o2[:weight]

g = makegem(13, 23000)
m = makemedicine(37, 222)

addweight(g,m) # = 50

Upvotes: 0

Bill
Bill

Reputation: 6086

Do you really need lots of types of structs at all for a small (n < 1000) list of widely varying things? Perhaps Julia's structs truly come into their own mainly when handling massed arrays of thousands of the same type. Are you planning that sort of parallel massiveness, or just a heterogenous list?

There is however a builtin type made for such a use case, the Dict.

GameObject = Dict{String, Any}

g = GameObject("name" => "diamond", "worth" => 23000)
m = GameObject("name" => "medicine", "healing_power" => 222, "worth" => 37)
coin = GameObject("worth" => 1)

This can work well. The minor annoyance that comes with it is the need for quotes for labels in the brackets, but that can be fixed with accessor functions:

# constructor...
newmedicine(worth, healingpower) = GameObject("name" => "medicine", 
    "worth" => worth, "healing_power" => healingpower)

name(g::GameObject) = try g["name"]; catch; "" end

for o in [g, m, coin]
    println(name(o))
end

Upvotes: 1

Related Questions