A transparent way for a decorator pattern (maybe without SimpleTraits)

Let’s say I have a type A a user should easily be able to extend by subtypes and write functions f, g, … and so on for. These functions are quite a few let’s say 20.

Furthermore a decorator pattern that decorates A additionally with B shall be used as illustrated in the following code

using SimpleTraits

abstract type A end
abstract type B end
@traitdef c{A,B}
struct A1 <: A end
struct A2 <: A end
struct A3{T<:A,S<:B} <: A
    a::T
    b::S
end

struct B1 <: B end
struct B2 <: B end

@traitimpl c{A1,B1}

@traitfn f(a::A3{T,S},x) where {T <: A, S <: B; c{T,S}} = f(a.a,x) # generic lazy fallback on the non decorated case
@traitfn f(a::A3{T,S},x) where {T <: A, S <: B; !c{T,S}} = error("No fallback")

where we then have the implementations

f(a::A1,x) = x/2 # a lazy default fall back to default
f(a::A3{A1,B2},x) = 2*x # an explicit non-lazy second case
f(a::A2,x) = x-1 # even lazier not using any B
f(a::A3{A2,B2},x) = x-1 # needs explicit implementation, since c{A2,B2} is not a trait

and while I like the lazy lines 1 and 3 and I am fine with the second line if one has an explicit second, non default (non-trait c) default – I have one thing that I don’t like:

One has to provide a line like the fourth for evey function like f as soon as the decorator is like B2 for A1, i.e. not a lazy fallback.

What I would like to have is the following (even a little more generic)

  • If some d::A3{T,S} type appears for fs first argument and there is no explicit implementation like 2 or 4, do a lazy fallback, i.e. fall f with d.a.
  • Implement this in a style like convert, such that it works for all functions where some A argument (like fs first) appears, for me that also sounds like convert
  • avoid SimpleTraits if there is a simpler solution.

One of my ideas is to use something like

convert(::Type{A1},a::A3{A1,B1}) = a.a 

instead of the trait, but I don’t seem to get that working. The goal is to allow for something like the lazy fallback by trait just for all functions like f.

Can something like that be done with conversions?

IMHO using SimpleTraits mostly over engineers traits, they are simple already… Just write them out, it will make it easier to refactor this and simplify the problem.

And try to remove the dispatch on nested types (it’s maybe even a code smell?), instead dispatch on the combinations manually, using A1, B2 etc like traits, instead of creating another trait. That allows you to keep adding combinations in a fairly straightforward way

f(a::A1,x) = x/2 # a lazy default fall back to default
f(a::A2,x) = x-1 # even lazier not using any B
f(a::A3,x) = f(x(a), y(a), a, x)
f(::A1, ::B2, a::A3, x) = 2*x # an explicit non-lazy second case
f(::A2, ::B2, a::A3, x) = x-1

Where x() and y() are getters for A and B objects/types. If I understand what you are trying to do.

3 Likes

Thanks for the response, so the getters would be

y(a::A3) = a.b
x(a::A3) = a.a

?

Actually that SimpleTraitsare a little too much is something I noticed when working on the theme behind this question :wink: I might actually try more to avoid them and in total (having again about 10-20 functions of this type in mind) I favour your approach. Though in a few cases I might still dispatch on a subtype of A3 (mostly for laziness and really not often).

Thanks for the tip on getters and the third argument idea.

Yes exactly that. Multiple dispatch really frees up this kind of problem as you can just add more arguments to the same method to dig down into increasingly specific dispatch. You end up with a stack of clear one-line specifications that are pretty easy to reason about.

I have a blanket rule against nested dispatch these days, but I’m sure you can find a few examples in my code somewhere…

2 Likes

With the rework following your remark, I was able to remove nearly all nested dispatch already and with a forthcoming update from a colleague, we will further move towards also avoiding the remaining once (at least I hope for that).

And I really like the one-line-specifications since they provide a quite clear view on the approach that had been chosen/modelled.

1 Like