abstract type AbstractTransport end
struct Warp
limit::Float32
end
struct Galaxy <:AbstractTransport
name::String
drive::Warp
end
struct Intrepid <:AbstractTransport
name::String
drive::Warp
end
Both Galaxy and Intrepid share the same field names of name::String and drive::Warp. On the surface it isn’t too bad, but consider if the number of fields grow and the introduction of an inner constructor for validation, that’s a lot of code that would be repeated over again.
Is there an acceptable pattern to handle this? The only work around I can think of is to separate the common fields into another struct, ShipDetails:
struct ShipDetails
name::String
drive::Warp
function ShipDetails(name::String, drive::Warp)
# validation
new(name,drive)
end
end
I can restructure the structs like this:
struct Galaxy <:AbstractTransport
attributes::ShipDetails
end
struct Intrepid <:AbstractTransport
attributes::ShipDetails
end
I basically made GitHub - marius311/CompositeStructs.jl for this exact issue. It’s definitely not a super established solution (the package isn’t even registered), but it has worked well for me thus far and I’m curious to keep developing it. From the readme:
Splices the fields of one struct into another. E.g.:
struct Foo{X,Y}
x :: X
y :: Y
end
@composite struct Bar{X,Y,Z}
Foo{X,Y}...
z :: Z
end
# equivalent to defining:
struct Bar{X,Y,Z}
x :: X
y :: Y
z :: Z
end
There’s a couple other similar packages out there mentioned in the readme, although this has the unique feature that you don’t need to do anything special to the “parent” struct.
I think this is a good approach. It doesn’t require any tools, and it makes the set of shared fields explicit. You can also use @forward from Lazy.jl to make it easier to access the fields inside attributes without having to do ship.attributes.drive.
The composition approach that you’ve described also makes it easy to see how you might create multiple different sets of shared attributes (after all, there are probably some fields in common to a warp-capable ship and other fields in common for a combat ship) without having to worry about multiple inheritance and the complexities that it creates in other languages.
I would also go with composition, or possibly a field (see below, preferable) or a type parameter:
using ArgCheck
struct Galaxy end
struct Intrepid end
struct Ship{T}
kind::T
name::String
drive::Warp
function Ship(kind::T, name, drive) where T
@argcheck warp <= 10 "you must be desperate for a plot device"
new{T}(kind, name, drive)
end
end
Ship(Galaxy(), "Enterprise-D", 9.6)
You can also make kinds a subtype of some abstract type, etc.
If all AbstractTransport subtypes have the same fields, you could also turn AbstractTransport into a concrete type and use a type parameter to distinguish different kinds:
abstract type AbstractTransport end
transports = [:Galaxy, :Intrepid]
struct Warp
limit::Float32
end
for transport in transports
@eval struct $transport <: AbstractTransport
name::String
drive::Warp
end
end
What I like about it is that if you come up with another ship name, you can just add it to transports. You can use the structs the same way as in your definition:
I’ll admit that having 2 separate structs simply because a ship is a different class is overkill. Perhaps a better idea would be StarShip <:AbstractTransport and CruiseShip <: AbstractTransport. Both differ in function to be different structs.
Just as an example of another approach,
here is a very shallow abstract type chain. (not really appropriate in this small example, still good to know)
abstract type AbstractTransport end
abstract type StellarTransport <: AbstractTransport end
abstract type PlanetaryTransport <: AbstractTransport end
struct Warp
limit::Float32
end
struct StarShip <: StellarTransport
name::String
drive::Warp
end
struct CruiseShip <: PlanetaryTransport
name::String
drive::Warp
end
galaxy = StarShip("Galaxy", Warp(10))
cruiser = CruiseShip("Cruiser", Warp(2))
function maxwarp(ship::StellarTransport)
return Warp(ship.drive.limit * 7/8)
end
function maxwarp(ship::PlanetaryTransport)
return Warp(ship.drive.limit * 5/8)
end
I was just thinking about this not too long ago! Very nice.
In this case, I would make Warp into Engine or better, make it abstract. Inside CruiseShip, I would make an Engine[], they might have a primary and backup engine, and in StellarTransport, I would do the same, because you often hear warp or impulse engines.