Using containers with abstract type parameters

Consider the following code:

abstract type GameObject end
abstract type Weapon <: GameObject end

struct Sword <: Weapon
	name::String
    damage::Int32
end

struct Bow <:Weapon
    name::String
    damage::Int32
end

mutable struct Character
    name::String
    inventory :: Vector
	function Character(name::String)
		inventory = Vector{Weapon}()
		new(name,inventory)
	end
end
queen_svetlana = Character("Queen Svetlana")
push!(queen_svetlana.inventory,Sword("Sword",10), Bow("Bow",5))

As stated in Performance Tips, it’s better to use a container with a concrete type, so instead of Real, you would use Float64.

The problem is in a situation where you want to keep an assortment of Weapons in one inventory, what is the efficient approach?

It seems kind of overkill to delcare a collection for each Weapon sub type:

swords = Vector{Sword}()
bow = Vector{Bow}()

In Performance Tips, it also states:

If you cannot avoid containers with abstract value types, it is sometimes better to parametrize with Any to avoid runtime type checking.

The problem is, with Any you could also add String and Int for a container meant only for Weapon. You could work around the problem by writing your own push!() function that takes a character and weapon, but nothing stops you from accessing the container directly.

I guess some context might be useful here:

  • Unlike a Vector{Float64}, we’re not doing any calculations with the Vector{Weapons}. We’re simply withdrawing a weapon to be used, and then putting it back afterwards.

  • Using a for loop and isa() we might filter certain weapons, if you want a brief summary of specific weapon types, and length(inventory) to get the number of weapons.

Does the Avoid containers with abstract type parameters tip depend on how the container will be used? Given the above situation of adding, removing and simply looping, would using Weapon be okay?

4 Likes

Hopefully some other folks will respond with thoughts but here is my first take.

Do your different weapons need to be different types? I.E. are you dispatching on different weapons? If not you could instead make Weapon concrete and store the kind of weapon as a field

struct Weapon <: GameObject
  name::String
  damage::Int32
  kind
end

Then you could have

mutable struct Character
  name::String
  inventory::Vector{Weapon}
  function Character(name::String) = new(name,Vector{Weapon}())
end

queen_svetlana = Character("Queen Svetlana")
push!(queen_svetlana.inventory, Weapon("Sword",10, sword), Weapon("Bow",5,bow))

The kind field could be a string “sword”, “bow”, etc. or even better would be to use an enum
@enum WeaponType sword bow bigrock the kind field could then be kind::WeaponType

On the other hand if you are dispatching on weapon types than this wouldn’t work and hopefully some other kind folks will have some advice for you.

One other point: you have a “name” field in your weapons. Is this duplicating the type? as Sword(“Sword”) is a bit redundant if you are using them a bit like the kind field I described above.
That is unless you literally mean to have named weapons like Axe("Brian's Flaming Axe",8) or Sword("Sting",10).

4 Likes

Upon further reflection you can do one better and make Weapon a parametric type using the enum I described above.

struct Weapon{T} where {T<: WeaponType} <: GameObject
  name::String
  damage::Int32
end

mutable struct Character
  name::String
  inventory::Vector{Weapon{WeaponType}}
  function Character(name::String) = new(name,Vector{Weapon{WeaponType}}())
end

queen_svetlana = Character("Queen Svetlana")
push!(queen_svetlana.inventory, Weapon{Sword}("Fiery Sword",10), Weapon{Bow}("bow",5))

This lets you have a concrete array (I think) and dispatch on weapon type if needed.

No this does not let you have a concrete array, it does not even parses. The syntax for the struct is:

julia> abstract type GameObject; end

julia> abstract type WeaponType <: GameObject; end

julia> struct Weapon{T <: WeaponType} <: GameObject
         name::String
         damage::Int32
       end

And if WeaponType is an abstract type or if it is an Enum (and you intend to do Weapon{BOW}(...) for example), both cases end up with inventory::Vector{Weapon{WeaponType}} being a container of an abstract type.

@anon60034542 If you need a container of objects of different types, just use a container of objects of different types. If you wanna them to be checked by type to be sure they are compatible when saved to the array, then pay the price for it. The only “solution” is what Jordan have pointed out before: transpose things from type-space to value-space. If you are not using the types for dispatch, and you do not need a way to extend the types of weapons externally (i.e., you have all the code in your hands, and you are not thinking about open-source modding), then you can create a concrete Weapon object with a field called type that has enum values inside it, and use this field in conditionals where it is needed.

What makes using containers of abstract types slow is exactly the fact that “withdrawing a weapon to be used” (this is, getting an element from the container) is type-instable, i.e., the compiler cannot optimize any of the code using such weapon because it is not sure which weapon it is, the type of the weapon may vary in runtime so it always need to make slow dispatch look-ups when interacting in any way with the “withdrawn weapon”.

Maybe if you are just copying it to another point into the array or to another container then the loss is small but even so, the fact the container can contain multiple types will probably make it an array of references, instead of just having the objects contiguous in memory.

It would be good to benchmark this, using a solution with a container of abstract objects against a container of concrete objects that have weapon_type field. But I think that it should not matter for length (because this does not interact with the elements themselves) and the for with isa may be slower, but isa is a built-in function (so kinda magical) and maybe more optimized than most generic functions that need to pass by the dispatch system normally. I doubt isa can be faster than checking a weapon_type field in a concrete object, however, at most it will be equivalent.

it does not even parse

Apologies, I was typing quickly to get the point across not to make perfectly syntax correct code. I should’ve made that clear so someone would know not to copy it exactly.

Thanks for letting her and I know it would still be an abstract type array. I apologize for my error.

Do your different weapons need to be different types? I.E. are you dispatching on different weapons? If not you could instead make Weapon concrete and store the kind of weapon as a field.

I chose Sword and Bow, because although both cause damage, both do so in different ways. I can swing my sword and hit an enemy or object (a shield, or another weapon), but with a bow, I can aim and then let my bow go from a distance, and use another bow (reloadable). You can look at it in many ways, for example, if I introduce Battle-Axe, that isn’t much different from a Sword, so instead of Sword I could say, SwingableWeapons.

One other point: you have a “name” field in your weapons. Is this duplicating the type? as Sword(“Sword”) is a bit redundant if you are using them a bit like the kind field I described above.
That is unless you literally mean to have named weapons like Axe("Brian's Flaming Axe",8) or Sword("Sting",10) .

You’re correct! It’s meant for fantasy type naming, like Ivan the Barbarian's Sword.

Maybe I was too forcible, I apologize for that. I just did not want people with similar doubts ending up with the wrong impression after reading this thread.

Thanks for letting her and I know it would still be an abstract type array. I apologize for my error.

Note the array itself is a concrete type, the problem is that the type of the elements it contains is not concrete, there is a distinction.

1 Like

Just to clarify, when you say:

a container of objects of different types, just use a container of objects of different types. If you wanna them to be checked by type to be sure they are compatible when saved to the array, then pay the price for it

Do you mean like I’m doing it now with:

inventory = = Vector{Weapon}()
push!(queen_svetlana.inventory,Sword("Sword",10), Bow("Bow",5))

Yes.

The thing is, if your case is really game development, then I really doubt the extra cost here is relevant (unless you are doing a simulation of huge armies swarming at each other). Your case seems to be “user is inspecting the weapons he have equipped in some character and maybe changing them” and in this case anything graphic (or even just printing the information in a terminal) will be much more costly than the cost of inspecting the list of weapons.

3 Likes

And what about just using an Union ?

So you would do this:

inventory = Vector{Weapon}()
SwordOrWeapon = Union{Sword,Weapon}
sword :: SwordOrWeapon = Sword("Normal Sword",10)
push!(inventory,sword)

Now, the real question is, when adding and removing from a Vector{Weapon}, does it suffer the same performance problems as indicated above?