Dispatch on decorated (sub)types

I was just wondering on how one could do multiple dispatch when using a decorator pattern and subtypes.

Imagine the following example

abstract type A end
struct A1 <: A
    c::Int
end
struct A2 <: A
    c::Float64
end
struct decoA{B <: A} <: A
   a::A
   name::String
end
decoA(a::B, s) where {B <: A} = decoA{B}(a,s)

basically this leads to 4 possible types

a1 = A1(1)
a2 = A2(1.)
a3 = decoA(a1,"Hi.")
a4 = decoA(a2,"Moin.")

with method dispatch on the first two I would write something (don’t mind the logic, it’s merely to illustrate the dispatch question)

f(a::A1) = A1(a.c*2)
f(a::A2) = A2(a.c/2)

then you get

julia> f(a1)
A1(2)
julia> f(a2)
A2(0.5)

now to the question: I would like to implement f also for the deco-cases, such that
f(a3) is the same as decoA(f(a1),"Hi.") and f(a4) is the same as decoA(f(a2),"Moin."),
so to a certain extend, that f and decoAinteract in a quite transparent way.

What’s the most elegant way to achieve such a dispatch with decorator pattern based types?

Edit: I actually found the answer to my MWE myself, it’s f(a::decoA{T}) where {T <: A) = decoA(f(a.a),a.name)

but that was merely due to my too simple MWE.

I would like to have an easy dispatch for all types A1, decoA{A1}, decoB{A1}, where decoB is similar to decoA, i.e. also with one parameter type and still a subtype of A, Can something like that be made with dispatch?

Not sure I understood the full extent of your question, but maybe something like this suits your needs.

abstract type A end
struct A1 <: A
    c::Int
end
struct A2 <: A
    c::Float64
end

# intermediate superclass for all decorators
abstract type DecoA <: A end

# each decorator has its own additions...
struct NamedA{T <: A} <: DecoA
   a::T  # <-- this is what you meant, right? (instead of "a::A")
   name::String
end
# ... which can be retrieved using a specific method
decoration(x::NamedA) = x.name

# same for a second decorator
struct IndexedA{T <: A} <: DecoA
    a::T
    index::Int
end
decoration(x::IndexedA) = x.index


# Specific methods for bare, concrete `A` subtypes
f(a::A1) = A1(a.c*2)
f(a::A2) = A2(a.c/2)

# One method for all decorated `A` subtypes
f(x::T) where {T<:DecoA} = T(f(x.a), decoration(x))
julia> a1 = A1(1);
julia> a2 = A2(1.);
julia> a3 = NamedA(a1,"Hi.");
julia> a4 = NamedA(a2,"Moin.");
julia> a5 = IndexedA(a1, 42);
julia> for a in (a1, a2, a3, a4, a5)
           println("a = $a \t f(a) = $(f(a))")
       end
a = A1(1)                         f(a) = A1(2)
a = A2(1.0)                       f(a) = A2(0.5)
a = NamedA{A1}(A1(1), "Hi.")      f(a) = NamedA{A1}(A1(2), "Hi.")
a = NamedA{A2}(A2(1.0), "Moin.")  f(a) = NamedA{A2}(A2(0.5), "Moin.")
a = IndexedA{A1}(A1(1), 42)       f(a) = IndexedA{A1}(A1(2), 42)
3 Likes

Retrieving the information with a specific function is quite neat. And sorry for the confusion, maybe I was not yet sure what I want myself. That’s why I tried to extend my question already. So for the initial part, your answer is very neat, I think I am looking for a function, let’s say g that dispatches on the undecorated type even for (eventually multiple) decorators.

with your system maybe something like

  • g(a1) should print "I work like A1 and I am an A1"
  • g(a2) should ptint "I do something different and I am an A2"
  • g(a3) should print "I work like A1 and I am an NamedA{A1}(A1(2), "Hi.")
    and so on but also for a6 = IndexedA(a3, 23) I would like to have I work like A1 and I am an IndexedA{NamedA{A1}}(NamedA{A1}(A1(2), "Hi."), 23)".

I think my main problem is then actually this double-encapsualed matching – and yes I was not aware, that that was my problem in the beginning :wink: so sorry for the confusion concerning the initial topic.

1 Like

No worries. I’m still not sure I get what you want, but these are interesting questions, well worth the time we spend thinking about them IMO.

Would that cover what you were thinking about?

abstract type A end
struct A1 <: A
    c::Int
end
struct A2 <: A
    c::Float64
end

# intermediate superclass for all decorators
abstract type DecoA <: A end

# each decorator has its own additions
struct NamedA{T <: A} <: DecoA
   a::T  # <-- this is what you meant, right? (instead of "a::A")
   name::String
end

struct IndexedA{T <: A} <: DecoA
    a::T
    index::Int
end


# Behaviour of bare, concrete `A` subtypes
f(a::A1) = "A1 behaviour ($(a.c*2))"
f(a::A2) = "A2 behaviour ($(a.c/2))"
f(a::DecoA) = f(a.a)

# Global behaviour of decorations
decoration(a) = ""
decoration(a::DecoA) = decoration_(a) * decoration(a.a)

# Specific behaviour of decorations
decoration_(a::NamedA) = " with name $(a.name)"
decoration_(a::IndexedA) = " with index $(a.index)"

# Combination of base behaviour and decoration
g(a::T) where {T<:A} = f(a) * decoration(a) * " and I'm an $T"

# Test
a1 = A1(1);
a2 = A2(1.);
a3 = NamedA(a1,"Hi.");
a4 = NamedA(a2,"Moin.");
a5 = IndexedA(a1, 42);
a6 = IndexedA(a3, 23);
for a in (a1, a2, a3, a4, a5, a6)
    println("a = $a \t $(g(a))")
end
a = A1(1)                        A1 behaviour (2) and I'm an A1
a = A2(1.0)                      A2 behaviour (0.5) and I'm an A2
a = NamedA{A1}(A1(1), "Hi")      A1 behaviour (2) with name Hi and I'm an NamedA{A1}                                                                                 
a = NamedA{A2}(A2(1.0), "Moin")  A2 behaviour (0.5) with name Moin and I'm an NamedA{A2}                                                                     
a = IndexedA{A1}(A1(1), 42)      A1 behaviour (2) with index 42 and I'm an IndexedA{A1}                                                                              
a = IndexedA{NamedA{A1}}(NamedA{A1}(A1(1), "Hi"), 23) 
                                 A1 behaviour (2) with index 23 with name Hi and I'm an IndexedA{NamedA{A1}}                                 

Ah, that’s again very neat! Thanks I learned quite a lot. I think I also got to the last thing that I am still thinking about, whether it’s still possible.
With just the first few lines

abstract type A end
struct A1 <: A
    c::Int
end
struct A2 <: A
    c::Float64
end

My original function g (before starting decorating) had three dispatch choices

g(a::A1) = "Inty"
g(a::A2) = "Floaty"
g(a::T) where {T <: A} = error("You might want to consider implementing g for your A-subtype $(T).")

merely to help users, that implement something that they just missed g when implementing a certain interface (consisting maybe of 3 or 4 functions within a framework working o Asubtypes).

This last line is the only one I miss, because g now can not do that anymore. That’s why I asked for a signature that dispatches on anything decorating with most inner type A1, with that I could say, that for any type (undecorated) I could provide again an error for g.

Long story short – I think only f can now take over this error message, because with its implicit “undecoration” for a::DecoA inputs (something I learned in this thread – and one could also do this with Traits instead of a common decotype).

Why is that a problem? f is the function that new A bare subtypes have to implement anyway, isn’t it? Likewise, decoration_ is the function for which new decorators should implement a method, and the default error message (missing method) can be overriden with a more user-friendly one.

Or am I missing something?

I don’t know why that was a problem for past-me, maybe I was too stuck in a strict dispatch idea.
And yes, with dectoration_ one could also generate a beautiful error message. Avoiding a just missing method-message and provide more hints what’s actually to do for fixing the error is what I am aiming for there.

1 Like