Only Abstract Types in Method Signatures

Reading @JonasWickman’s comment that views generally use abstract types for their method definition made me think. If the method has at least one type in its signature this type needs to have an abstract parent and if Any isn’t concrete enough, this needs to be a more specific one like AbstractMyType. As basically all types might be used in a view and they might want to define some Base methods, it follows, that basically all types should have such a parent type.

The open–closed principle demands that modules keep open for extension, i.e. it should be possible to reuse the methods if the type gets extended (in Julia: by a new type with identical fields + new ones). Indeed, when using a type from a package and the type needs to be extended it’s a bit awkward to do this if methods defined for this type use the concrete type. If they use something like AbstractMyType it’s again much easier.

Up to now I only used abstract types if I had already more than one subtype planned. However, above thoughts made me wonder whether I should switch my default and just create/use an abstract type for each of my types (at least the exported/public ones) as default even if I do not yet plan to have multiple subtypes of these abstract types. Obviously I would not define abstract types for types I do not own, but otherwise I would avoid concrete types in method signatures.

But before doing this, I want to collect some experience and insights from the community:

  • Is there any negative influence on type interference if method signatures only contain abstract types?
  • Is there any significant negative influence on compilation time?
  • Is there an increase in method ambiguities (if only one concrete type is defined for the abstract type – otherwise it would be okay as there is benefit)?
  • Are there any other downsides of defining so many abstract types (besides the obvious, i.e. extra abstract type definitions and the probably slightly longer type names)?
1 Like

Unless you use @nospecialize, type annotations of a method’s arguments only guide dispatch of function calls to the method. Each method is compiled multiple times for the concrete types of the calls’ inputs by default with the exception of instances of Function, Type, and Varargs.

type annotations of a method’s arguments only guide dispatch of function calls

Thanks! So, there’s no influence on type inference or method ambiguities, correct? I assume that the extra time needed to match the types for finding the right method to dispatch is negligible and should mostly be done statically anyway. Is this assumption correct?

If so, I wonder why we do not recommend this pattern in the documentation. At least, I haven’t seen it mentioned there.

Abstract type annotations of method arguments aren’t a pattern for enabling a feature or solving a limitation in the first place, it’s just a part of method dispatch. foo(a::AbstractArray) isn’t inherently more correct or beneficial than foo(a::Array); it just depends on whether you want a method that works on AbstractArrays or just Arrays. You may need both methods because you need to do something specific to Arrays that you can’t or shouldn’t put into the AbstractArray method; you can find examples in other languages that must instead put both versions in type-checking branches e.g. if a isa Array _foo_Array(a) else _foo_AbstractArray(a) end. Note that Julia’s compiler has been able to optimize away type-checked branches for a long time, but a practical reason to avoid that is editing the method forces recompilation of the larger method and the summed methods that called it.

The distinction between method dispatch and compiled specializations is documented here.

Your assumption is mostly correct except abstract type annotations indeed open up method ambiguities, and trivially so. If we were only able to annotate concrete types in methods, it’s impossible for a function call’s concrete types to match multiple methods, let alone a set that lacks a most specific one. We can draw a parallel in ambiguity from a Tuple{...} type containing a function call’s concrete types with multiple Tuple{...} supertypes representing methods to a class with multiple parent classes. There are principles for gracefully avoiding method ambiguities; unless you’re weaving a particularly complex web of methods, it’s easier than it looks.

1 Like

Fortunately, when all methods use only abstract type annotations, such as in:
mymethod(x::AbstractMyType1, y::AbstractMyType2, ...)
there should not be any method ambiguities as long as each of these abstract types has only one concrete subtype.

Of course, ambiguities can arise if a new subtype of one of these abstract types is added later, along with a specialized method. However, in that case, nothing is lost compared to not being able to reuse the method initially because the type annotation was too concrete (in hindsight).

That’s technically untrue, here’s a minimal counterexample of methods that only involve abstract types with 1 concrete subtype each:

julia> begin
         abstract type AAA end
         abstract type AA<:AAA end
         struct A <: AA end
         abstract type BBB end
         abstract type BB<:BBB end
         struct B <: BB end
         foo(::AAA, ::BB) = 1
         foo(::AA, ::BBB) = 2
       end;

julia> foo(A(), B())
ERROR: MethodError: foo(::A, ::B) is ambiguous.

I can also disprove the broader idea of a bijection from concrete types to their supertypes (excepting Any) making them interchangeable, but this minimal example breaks your stipulation of only abstract type annotations:

julia> begin
         abstract type CC end
         struct C <: CC end
         abstract type DD end
         struct D <: DD end
         foo(::C, ::DD) = 1
         foo(::CC, ::D) = 2
       end;

julia> foo(C(), D())
ERROR: MethodError: foo(::C, ::D) is ambiguous.

I would bet money that method ambiguities would indeed be impossible if we stipulate such a bijection in addition to abstract type annotations only, but anything is harder to prove than disprove, and it’s far more impractical to use or enforce than concrete type annotations anyway.

2 Likes

Sorry, my statement was ambiguous: I meant “only one subtype and that one is concrete, i.e. no abstract subtypes”. Whereas I think you understood “only one concrete subtype and an arbitrary amount of abstract subtypes”.

But also thanks to your examples I am quite sure that the statement holds in this specific case. In your examples it would mean that the method definitions only use the double character types, i.e. AA, BB, CC and DD.

Thinking more about it, while your general statement is correct, in this case the proof seems to be easy. As long as the double character types only have a single subtype, they are identical to the single character types, by all means, if the single character types are not used at all in method signatures. Therefore, the proof of unambiguous method calls for concrete types can be used as a proof for the case of single-concrete-no-abstract subtypes.

Obviously, this only helps until the second subtype is created and before that, there is no practical benefit. But at least it shows that you can’t lose with defining these additional abstract types while you might win in terms of extensibility if no method ambiguities occur later.