Reputation: 149
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
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
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
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