Idiom for related types?

Short version:

I want something that serves the role of “methods for structs”, where the content of the struct depends nontrivially on the parameter (just as the content of a method can depend on the type of an argument). Obviously I can have

struct MyStruct{T}
    field_a::T
    field_b::T
end

but it turns out that there are times when I want to have, say, two fields if T is Int and three if it’s String. The closest I can come up with is

struct MyStructForInt #= blah blah blah =# end
struct MyStructForString #= totally different =# end
MyStruct(::Type{Int}) = MyStructForInt
MyStruct(::Type{String}) = MyStructForString

This feels unwieldy, like I shouldn’t do it and I’m going to regret the decision later.
I’m wondering if there’s something a little more natural/idiomatic.

And since that’s probably not very clear, here’s a good deal of elaboration, trying to explain how this comes about.

Long version:

I have a set of (conceptually) related types. The classic cartoon version is something like:

abstract type Animal end
struct Cat <: Animal end
struct Dog <: Animal end

Now I want to be able to write generic code that acts on any Animal naturally. This is accomplished by defining a function with multiple methods

speak(::Cat) = println("meow")
speak(::Dog) = println("woof")

So far this is perfectly standard. Now suppose I want to write a function walk(::Animal). This might look like:

function walk(a::Animal)
    for leg in legs(a)
       move(leg)
    end
end

Of course I need a definition for legs. A cat’s leg is not the same object as a dog’s leg—for the purposes of this demonstration let’s assume that representing them requires different numbers of fields. Therefore different structs need to be defined, so I end up with

abstract type Leg end
struct CatLeg <: Leg #= stuff =# end
struct DogLeg <: Leg #= very different stuff =# end
legs(::Cat) = [CatLeg(), CatLeg(), CatLeg(), CatLeg()]
legs(::Dog) = [DogLeg(), DogLeg(), DogLeg(), DogLeg()]
function move(::CatLeg) #= move leg gracefully =# end
function move(::DogLeg) #= move leg less gracefully =# end

So far everything seems okay, although we might be worried that it’s not possible to add type annotations to the definition of walk above: what’s the type of leg?

Disaster strikes when I attempt to attach a fifth leg to the dog.

function addleg(a::Animal, l::Leg)
    #= Do something =#
end

This is not what I mean! Obviously it’s only okay to add a DogLeg to a Dog and a CatLeg to a cat. What I want to do is write something like

function addleg(a::A, l::Leg{A}) where {A <: Animal}
    #= Do something =#
end

But here, Leg{Dog} and Leg{Cat} would be forced to have the same structure, which they don’t.

Hope that made some sense (and wasn’t too gruesome). The actual case I have has several related types, analogous to having Leg and Paw and Head each defined for Dog and Cat and Elephant, with radically different structure. Having a nice way to go from T <: Animal to the appropriate Leg{T}-like type would make life much easier.

abstract type Leg{AnimalT <: Animal} end
struct CatLeg <: Leg{Cat} #= stuff =# end
struct DogLeg <: Leg{Dog} #= very different stuff =# end

Will this do the thing you need?

3 Likes

That might be the best thing. I’m still missing a capability I’d like:

function growleg(a::A) where {A <: Animal}
    # I want to call the constructor of either CatLeg or DogLeg
    return Leg{A}(a) # Nope, this is an error obviously
end

More abstractly, in what you wrote, it’s possible to have two different structs each of which is <: Leg{Cat} (and this is essentially why Leg{A}(a) isn’t correct). But conceptually that’s not the case, there’s a single struct which corresponds to Leg{Cat}, and so there ought to be a function that maps Cat to CatLeg, and Dog to DogLeg.

julia> struct MyStruct{T}
           fields::T
           MyStruct(a::Int, b::Int) = new{Tuple{Int,Int}}((a,b))
           MyStruct(a::String, b::String, c::String) = new{Tuple{String,String,String}}((a,b,c))
       end

julia> MyStruct(3,5)
MyStruct{Tuple{Int64, Int64}}((3, 5))

julia> foo = MyStruct(3,5)
MyStruct{Tuple{Int64, Int64}}((3, 5))

julia> foo.a
3

julia> foo.b
5

julia> foo.c

julia> function Base.getproperty(ms::MyStruct{Tuple{Int,Int}}, s::Symbol)
           if s == :a
               getfield(ms, :fields)[1]
           elseif s == :b
               getfield(ms, :fields)[2]
           else
               error("No field named $s.")
           end
       end

julia> function Base.getproperty(ms::MyStruct{Tuple{String,String,String}}, s::Symbol)
           if s == :a
               getfield(ms, :fields)[1]
           elseif s == :b
               getfield(ms, :fields)[2]
           elseif s ==:c
               getfield(ms, :fields)[3]
           else
               error("No field named $s.")
           end
       end

julia> foo = MyStruct(3,5)
MyStruct{Tuple{Int64, Int64}}((3, 5))

julia> foo.a
3

julia> foo.b
5

julia> foo.c
ERROR: No field named c.

julia> bar = MyStruct("Walk", "The", "Dog")
MyStruct{Tuple{String, String, String}}(("Walk", "The", "Dog"))

julia> bar.a
"Walk"

julia> bar.b
"The"

julia> bar.c
"Dog"

julia> bar.d
ERROR: No field named d.

Actually, not if you define this constructor for the abstract type :slight_smile:

Leg{<:Cat}(A) = CatLeg(A)
3 Likes

Okay I think this solves it. I’d played with defining constructors in weird ways but for some reason this hadn’t occurred to me. Thank you very much!