This might not be a great solution, but I’ve found myself doing things like this…
In the original post, I would define a big Person
type that has anything you’d want to know about a person, e.g.
struct Person
name::String
age::Int
nationality::Union{String,Nothing} # You should be able to add this without breaking any existing `Person` methods
end
then I would use Holy traits (as I understand them), e.g. define singleton types with a method to insert a type for later dispatch, e.g.
struct Citizen end
struct Outlaw end
persontype(a::Person) = isnothing(a.nationality) ? Outlaw() : Citizen()
nationality(a::Person) = nationality(a,persontype(a))
nationality(a::Person,::Outlaw) = error("Call the sheriff!")
nationality(a::Person,::Citizen) = a.nationality
Not an abstract type in sight. Using this trick makes me think of Person
as a “abstract type with fields”.
Similarly, in the more recent example, I would define an encompassing AbstractFoo
type
struct AbtractFoo{T}
a
b::Int
c::T
d::Vector{T}
e::Vector{T}
z::Union{String,Nothing}
end
struct Foo{T} end
struct AnotherFoo{T} end
footype(f::AbstractFoo{T}) where T = isnothing(f.z) ? AnotherFoo{T}() : Foo{T}()
dostuff(f::AbstractFoo) = dostuff(f,footype(f))
dostuff(f::AbstractFoo,::Foo) = # Do Foo stuff
dostuff(f::AbstractFoo,::AnotherFoo) = # Do AnotherFoo stuff
Edit: There is one more thing I do. I’d add constructor-looking methods:
Outlaw(name::String,age::Int) = Person(name,age,nothing)
Citizen(name::String,age::Int,nationality::String) = Person(name,age,nationality)
Similarly
Foo(a,b,c,d,e) = AbstractFoo(a,b,c,d,e,nothing)
AnotherFoo(a,b,c,d,e,z) = AbstractFoo(a,b,c,d,e,z)