How does a dictionary store structs?

Consider for a moment the following structs:

abstract type AbstractTransport end

struct Warp
    limit::Float32
end

struct Galaxy <:AbstractTransport
    name::String
    drive::Warp
end

struct Intrepid <:AbstractTransport
    name::String
    drive::Warp
    doctor::String # This is specific to Intrepid
end
uss_enterprise = Galaxy("USS Enterprise-D",Warp(9.0))
uss_voyager = Intrepid("USS Voyager",Warp(9.9),"Emergency Medical Hologram")

ships = Dict{String,AbstractTransport}((uss_enterprise.name => uss_enterprise),
                                        (uss_voyager.name => uss_voyager))

println(ships["USS Voyager"].doctor)

When both starships get stored in ships it’s stored as an AbstractTransport, why can I retrieve USS Voyager and access the doctor attribute?

I’m used to the OO model where anything declared in the subclass is specific to that subclass, and can’t be accessed by it’s parent. As you can see, this isn’t the case here, all the attributes are specific to each struct.

I guess the larger more accurate question would be, how are the structs stored in ships, so that I can access the specific attributes of each struct? (I’m looking at this from an OO perspective, which probably isn’t a good idea).

Right–you’re thinking of how things work in a language like C++, where there’s a difference between the compile-time type (e.g. AbstractTransport) and the run-time type (e.g. Intrepid). Julia doesn’t have this distinction at all (see this comment: Compile-time vs run-time actions - #3 by StefanKarpinski). No matter how you store an Intrepid or what generic container you use to hold it, Julia will always remember that it was an Intrepid and will always let you treat it as such.

As an extreme example, consider a Vector{Any} like []. In a language with different compile-time and run-time types, you wouldn’t be able to do anything with the contents of such an array, since the Any abstract type doesn’t provide any methods. This clearly isn’t the case in Julia, where you can store anything you want in a Vector{Any} and still access each individual element in its proper type.

As for how this is actually stored, it’s probably best to focus on what the language actually ensures, which is: No matter what the value type of your Dict is, an Intrepid will always be an Intrepid. In practice, if you have a dictionary with a non-concrete value type, then there will be some kind of metadata stored giving the actual type of each value. If you have a Dict{String, Intrepid}, then there’s no need for that metadata for each individual value, which is one reason why concrete container types are more efficient. I’m sure other more knowledgeable people can give you details on the exact form of that metadata (my mental model is “a pointer and a type tag”), but that’s probably only necessary to understand if you actually want to modify the internals of Julia itself.

3 Likes