Dispatch, Traits and Metaprogramming Over Reflection

By: oxinabox.github.io

Re-posted from: https://www.oxinabox.net/2018/10/03/Dispatch,-Traits-and-Metaprogramming-Over-Reflection.html

This is a blog post about about dispatch.
Mostly, single dispatch, though it trivially generalises to multiple dispatch,
because julia is a multiple dispatch language.

This post starts simple, and becomes complex.

  • Beginning with, introductory julia skills: dispatch
  • continue to intermidate julia skills: traits
  • and finishing up with advanced techniques: metaprogramming over reflection

The last of which is kinda evil.

Update: 11th of July 2021:
This post is fairly old now, but still gets frequently referenced.
I’ve done a bunch more stuff in this area since then.
I wrote a newer (and IMO better) blog post on traits for my employeer’s blog.
The Emergent Features of JuliaLang: Part II – Traits.
(I still think a better post still could be written.).
I have made Tricks.jl which actually does let you define traits based on if something has a method, and lets you resolve that at compile time.
I added signature to ExprTools.jl which is a much more robust way to do metaprogramming over reflection.
Which I will be talking about at JuliaCon 2021, slides are online now.

I think dispatch is very intutitive as a concept.
In C you learn that a function’s signature,
is composed of it’s name, and the types of the arguments and the return type.
So it would make sense that you could define a different function, depending on the types.
(but you can’t).

In a dynamic language you are supposed not worrying too much about the type of your values.
I feel that the emphisis should be on on not worrying too much: you need to worry just the right amount.
Duck-typing is great.
But reasoning about types is very easy.
It is the kind of reasoning we do all the time.

Dispatch is about striking the balance between exploiting knowledge of a type,
and being general.
I can write general code that doesn’t worry about the types,
and then add extra methods based on the types of the arguments for special cases.

Part 1: Displaying a Percentage

I would like to display a value as a percentage.
So that is pretty easy, if I have some portion, (e.g 0.5),
I can convert it to a percentage by multipling it by 100 (e.g. 100 × 0.5 = 50),
then I print it and append a % sign.

Input:

display_percent(x) = println(100x,"%") 

Output:

display_percent (generic function with 1 method)

That gives me a general rule that works for most types.

Input:

display_percent(0.5) #Float64
display_percent(0.5f0) #Float32
display_percent(BigFloat(π)/10) #BigFloat
display_percent(1) #Int64
display_percent(false) #Bool
display_percent(2//3) #Rational

Output:

50.0%
50.0%
3.141592653589793238462643383279502884197169399375105820974944592307816406286212e+01%
100%
0%
200//3%

I’m not exactly happy with how that Rational displayed,
it is correct, but I would rather it was displayed not as a fraction.
So we can fix it by adding a new method for Rationals.

Our current method has no type constraints.
We will create one that has a constraint that the argument must be a Rational.
More specific methods, i.e. ones with tighter type constraints are called over ones with looser constraints.

Input:

display_percent(x::Rational) = println(round(100x; digits=2), "%")

Output:

display_percent (generic function with 2 methods)

Input:

display_percent(2//3) #Rational

Output:

66.67%

That worked great.

What if I am given a String as input,
that is already a percentage?

Input:

display_percent("5.81%")

Output:

MethodError: no method matching *(::Int64, ::String)
Closest candidates are:
  *(::Any, ::Any, !Matched::Any, !Matched::Any...) at operators.jl:502
  *(!Matched::Missing, ::AbstractString) at missing.jl:139
  *(::T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8}, !Matched::T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8}) where T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8} at int.jl:54
  ...



Stacktrace:

 [1] display_percent(::String) at ./In[1]:1

 [2] top-level scope at In[5]:1

We get an error, because there is no method to do 100 times a String;
and multiplying the string by 100 isn’t what we want to do anyway.
So we can define a special method for how to display a string as a percent.
It is going to check the string is in the correct format,
and if so display it.

Input:

function display_percent(str::AbstractString)
    if occursin(r"^\d*\.?\d+%$", str)  # any combination of numbers, followed by a percent sign
        println(str)
    else
        throw(DomainError(str, "Not valid percentage format"))
    end
end

Output:

display_percent (generic function with 3 methods)

Input:

display_percent("5.81%")

Output:

5.81%

Great, fixed it.

Where this really comes in handy is working with things you as the programmer don’t know the type of, when you are writing the code.
For example if you have a hetrogenous list of elements to process.

Input:

for x in [0.51, 0.6, 1//2, 0.1, "2%"]
    display_percent(x)
end

Output:

51.0%
60.0%
50.0%
10.0%
2%

Real world example of this sort of code is in solving for indexing rules in TensorFlow.jl.

So dispatch is useful, it lets you write different rules for how to handle different types.
Dispatch goes very nicely in a dynamically typed language.
Since (type-unstable) functions can return different types, depending on inputs,
and some types (like our String and our Rational) might want to be handled differently,
we need a good way of dealing with those cases.

It is also nicely extensible.
Lets say we created a singleton type to represent a half.

Input:

struct Half
end

display_percent(::Half) = println("50%")

Output:

display_percent (generic function with 4 methods)

Input:

display_percent(Half())

Output:

50%

So it was simple to just add a new method for it.

Users can add support for new types,
completely separate from the original definition.

Constrast it to:

Input:

function display_percent_bad(x)
     if x isa AbstractString
        if occursin(r"^\d*\.?\d+%$", x)  # any combination of numbers, followed by a percent sign
            println(x)
        else
            throw(DomainError(x, "Not valid percentage format"))
        end
    else
        println(100x, "%")
    end
end

Output:

display_percent_bad (generic function with 1 method)

Input:

display_percent_bad("5.3%")
display_percent_bad(0.5)
display_percent_bad(Half())

Output:

5.3%
50.0%
MethodError: no method matching *(::Int64, ::Half)
Closest candidates are:
  *(::Any, ::Any, !Matched::Any, !Matched::Any...) at operators.jl:502
  *(::T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8}, !Matched::T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8}) where T<:Union{Int128, Int16, Int32, Int64, Int8, UInt128, UInt16, UInt32, UInt64, UInt8} at int.jl:54
  *(::Union{Int16, Int32, Int64, Int8}, !Matched::BigInt) at gmp.jl:463
  ...



Stacktrace:

 [1] display_percent_bad(::Half) at ./In[11]:9

 [2] top-level scope at In[12]:3

You can see, that since it doesn’t include support for Half when it was declared,
it doesn’t work,
and you can’t do much to make it work.

This kinda code is common in a fair bit of python code.

Part 2: AsList, (including using traits)

The kind of code which uses if conditionals,
is quite common in practice.

In Python TensorFlow has a helper function, _AsList:

def _AsList(x):
    return x if isinstance(x, (list, tuple)) else [x]

It’s purpose is to convert scalar values,
into lists with one item.
So it checks if the input is a list or a tuple,
and if it is, then it makes no change,
if not then it wraps it in a list.

Problem with this is,
what if my input is a type it didn’t expect.
But which actually
duck-types as a list, as fair as the functionality required is concerned.
e.g. perhaps a dequeue.
Then the code will break.
My dequeue will be mangled into a list containing a deque,
which won’t index or iterate correctly.
And there is nothing I can do about it, except make a PR to TensorFlow.
(or perhaps as a hack, monkey-patch it).

Example 2.1 direct

The basic way would be to write methods for each.
Like before.

Input:

aslist_direct(x) = [x]
aslist_direct(x::Union{AbstractArray,Tuple}) = x

Output:

aslist_direct (generic function with 2 methods)

The Union is just a way of saying the argument needs ot match either of these types.

Input:

@show aslist_direct([1,2,3])
@show aslist_direct(1);

Output:

aslist_direct([1, 2, 3]) = [1, 2, 3]
aslist_direct(1) = [1]

What we are saying is that scalar values do one thing,
and nonscalar values do another.
We can add more rules to aslist_direct.
Though we can’t add more types to the Union so we would have to declare them separately,
which would involve repreated code.

e.g.

Input:

aslist_direct(x::String) = x

Output:

aslist_direct (generic function with 3 methods)

Repeating code is bad.
Further what if we want the notion of Scalarness elsewhere in our code?

One way to handle this and avoid repeating code, would be to use abstract types.
If all our scalar things were a subtype of AbstractScalar,
and all out nonscalar things were not,
then we could just write:

aslist(x::AbstractScalar) = [x]
aslist(x) = x

But the types we are concerned with already have super-types (and you can only have one, not multiple inheritance here),
and are already declared.
Abstract supertypes are bit of a gun with only one bullet;
and that bullet needs to be fired when they are declared.

Instread we can use Traits.
There are packages for traits in julia,
but you don’t really need them.
We are just going to use Holy Traits (named for Tim Holy, who came up with the idea).

Example 2.2 Traits

A trait in julia is basically just a pure function of a type, which returns a typed value that is used for dispatch.
They are used to implement a bunch of rules for handling things related to indexing, broadcasting, iterators etc.

Traits are uesful because they let you make declarative statements about a type.
Declarative code is easy to write and read.

Also, note that they are fast.
Since they are pure functions of their input type,
and Julia compiles (and optimises) specialised versions of every function for each combination of input types,
they are compiled away to just being direct static dispatches to the targeted methods.
The trait functions themselves are not actually evaluated at runtime.

Explaining this is going to take a little bit,
but I think traits are an important concept as you move from basic use of julia to something more advanced.
They are are really expressive.
So I think they are worth understanding.

Types are values and so have a type

Before we start it is important to understand, is that types are first class in julia.
They are values and they thus have a type.

Input:

@show typeof(String)
@show typeof(Int)

Output:

typeof(String) = DataType
typeof(Int) = DataType
DataType

Input:

String isa DataType

Output:

true

So the type of a type (such as String) is DataType.
A special thing for types though,
is they also act as if they are instances of the special type called Type.
This is a parametric type,
and it basically has a rule that T <: Type{T}.
Every type T is considered as if it were a subtype of Type with the parameter T.

It can be used for value-type dispatch.
(Which I will not be going into here, much more,
but see my StackOverflow post on it)

Input:

@show String isa Type{String}
@show String isa Type{<:AbstractString}

Output:

String isa Type{String} = true
String isa Type{<:AbstractString} = true
true

Mostly, we will want to use triangular dispatch rules.
That is the form Type{<:AbstractString},
also written Type{T} where T<:AbstractString,
which allows use to place type constraints on the type parameters.

Trait Types

To actually create the trait
we will need to define types for the trait.
This is what the trait function will return.
These should be concrete types.
They don’t need any fields – and indeed generally trait types shouldn’t have any fields if possible (it is better to give them type parameters.)
Though they could have super-types,
and is often good to give them a super-type to make them similar.

Input:

abstract type Scalarness end
struct Scalar <: Scalarness end
struct NonScalar <: Scalarness end

Trait Functions

We are now going to create the trait functions.
These should functions only of the type,
not of the value, so that they can be optimised away at compile time.

Input:

scalarness(::Type) = Scalar() #fall-back, by default everything is scalar
scalarness(::Type{<:AbstractArray}) = NonScalar()
scalarness(::Type{<:Tuple}) = NonScalar()
scalarness(::Type{<:AbstractString}) = NonScalar()

Output:

scalarness (generic function with 4 methods)

Nice, declarative code.
Users can add the scalarness type to their types
similarly.

Input:

@show scalarness(typeof([1,2,3]))
@show scalarness(typeof(1));

Output:

scalarness(typeof([1, 2, 3])) = NonScalar()
scalarness(typeof(1)) = Scalar()

Notice below that it is fully inferable and type-stable.
This will result it being optimised away at compile time during specialisation.

Input:

@code_warntype scalarness(typeof([1,2,3]))

Output:

Body::NonScalar
│2 1 ─     return $(QuoteNode(NonScalar()))

This is what makes it fast.

Dispatching on Traits

The whole reason we defined these is so we can use them for dispatch.
We will thus create three functions.

  • One for aslist of scalar types,
  • one for aslist of nonscalar types,
  • and one to re-dispatch on the trait value.

We will begin with the last since it is the first that will be called.

Input:

aslist(x::T) where T = aslist(scalarness(T), x)

aslist(::Scalar, x) = [x]
aslist(::NonScalar, x) = x

Output:

aslist (generic function with 3 methods)

Notice how the first is evaluating the trait function(on the type).
The return type of this is used for the dispatch to one of the other two.

Input:

@show aslist([1,2,3])
@show aslist(1);

@show aslist(Set([1,2]));

Output:

aslist([1, 2, 3]) = [1, 2, 3]
aslist(1) = [1]
aslist(Set([1, 2])) = Set{Int64}[Set([2, 1])]

Notice that the Set was treated as a scalar, and wrapped in to an array.
Adding a new type is easy.

Input:

scalarness(::Type{<:AbstractSet}) = NonScalar()
@show aslist(Set([1,2]));

Output:

aslist(Set([1, 2])) = Set([2, 1])

Part 3: Traits, falling back to hasmethod

A more advanced stratergy is would be to fall back to hasmethod

Input:

using LinearAlgebra # Load up LinearAlgebra for more types that might be interesting to know scalarness on

Input:

using InteractiveUtils: hasmethod
scalarness(::Type{T}) where T = hasmethod(Base.iterate, (T,)) ? NonScalar() : Scalar()

Output:

scalarness (generic function with 5 methods)

Now this makes may thing things work without having to define the scalarness trait,
though you would be surprised how many things have iterate defined.

Input:

@show scalarness(Dict)
@show scalarness(Int);

@show aslist(1)

Output:

scalarness(Dict) = NonScalar()
scalarness(Int) = NonScalar()
aslist(1) = 1
1

All number types have the iterate function defined on them.
So we probably want to overwrite that.

Input:

scalarness(::Type{<:Number}) =  Scalar()

@show scalarness(Int);

Output:

scalarness(Int) = Scalar()

Input:

aslist(1)

Output:

1-element Array{Int64,1}:
 1

Also, notice that it is now no longer a nice clean static dispatch.
Because it depends on the global state of the method table.
Which will mean that it has to resolve the trait at run-time.
So it will be slower.

Input:

@code_warntype(scalarness(Dict))

Output:

Body::Union{NonScalar, Scalar}
│╻╷        hasmethod2 1 ─ %1  = π (Tuple{Dict}, Type{Tuple{Dict}})
││┃│        #hasmethod#19  │   %2  = π (typeof(iterate), Type{typeof(iterate)})
│││╻         signature_type  │   %3  = (Base.getfield)(%1, :parameters)::Core.SimpleVector
││││        │   %4  = π (%3, Core.SimpleVector)
││││        │   %5  = π (%2, Type{typeof(iterate)})
││││        │   %6  = (Core.tuple)(Base.Tuple, %5)::Tuple{DataType,DataType}
││││        │   %7  = π (%4, Core.SimpleVector)
││││        │   %8  = (Core._apply)(Core.apply_type, %6, %7)::Type{Tuple{typeof(iterate),Dict}}
│││╻         getproperty  │   %9  = (Base.getfield)(typeof(iterate), :name)::Core.TypeName
││││        │   %10 = (Base.getfield)(%9, :mt)::Any
│││         │   %11 = π (%8, Type{Tuple{typeof(iterate),Dict}})
│││         │   %12 = $(Expr(:foreigncall, :(:jl_method_exists), Int32, svec(Any, Any, UInt64), :(:ccall), 3, :(%10), :(%11), 0xffffffffffffffff, 0xffffffffffffffff))::Int32
││││╻╷╷╷╷╷    ==  │   %13 = (Core.sext_int)(Core.Int64, %12)::Int64
│││││╻         ==  │   %14 = (%13 === 0)::Bool
││││╻         !  │   %15 = (Base.not_int)(%14)::Bool
│           └──       goto #3 if not %15
│           2 ─       return $(QuoteNode(NonScalar()))
│           3 ─       return $(QuoteNode(Scalar()))

Though types that we have defined an explict rule for will still be fast.

However, types that we have an more specific trait method for will still be fast,
since they will not hit the type-unstable fallback.

Input:

@code_warntype(scalarness(Int))

Output:

Body::Scalar
│1 1 ─     return $(QuoteNode(Scalar()))

On a theoretical level, hasmethod could be made type-stable and inferable;
which would allow for the static dispatch that we see for the explict cases.
This might be a thing in future versions of Julia.
It requires triggering recompilations of code depending on the lists of methods when they change.
There is some discussion of that here..

Part 4: Hard-core, reflecting upon methods

To avoid the dynamic dispatch that needs to be done for hasmethod,
we could declare all the scalarness of all types we know to have iterate methods.

This can be done could use reflection and metaprogramming,
which will generate a set of trait methods,
based on the current state of the of method table.

Be warned this gets intense.

Notice the methods that exist for iterate: (I’ll just show the first five).

Input:

collect(methods(Base.iterate))[1:5]

Output:

[1] iterate(v::Core.SimpleVector) in Base at essentials.jl:578
[2] iterate(v::Core.SimpleVector, i) in Base at essentials.jl:578
[3] iterate(ebo::ExponentialBackOff) in Base at error.jl:171
[4] iterate(ebo::ExponentialBackOff, state) in Base at error.jl:171
[5] iterate(m::Base.MethodList, s...) in Base at reflection.jl:730

We can extract their parameter types.

Input:

mm = first(methods(Base.iterate))

Output:

iterate(v::Core.SimpleVector) in Base at essentials.jl:578

Input:

mm.sig

Output:

Tuple{typeof(iterate),Core.SimpleVector}

Note that that this is not a Tuple value, that is a Tuple type.
First type-param is the function, and the rest are the arguments.
We are actually going to write dispatch based rules to create out trait functions.

Input:

try_get_single_argtype(f::Function) = map(try_get_single_argtype, methods(f)) # check each method of function

try_get_single_argtype(mm::Method) = try_get_single_argtype(mm.sig)

try_get_single_argtype(x::Any) = nothing # Fail, return nothing to indicate failure.

# We are only intrested in doing things for the 1 argument signature.
# which is the 2-tuple
try_get_single_argtype(::Type{Tuple{F, T}}) where {F, T} = T

Output:

try_get_single_argtype (generic function with 4 methods)

Input:

try_get_single_argtype(mm)

Output:

Core.SimpleVector

Input:

try_get_single_argtype(methods(Base.iterate).ms[2])==nothing

Output:

true

Now we can go through and filter based on if try_get_single_argtype returned nothing,

Input:

is_nothing(::Nothing) = true
is_nothing(::Any) = false

nonscalar_types = unique(filter(!is_nothing, try_get_single_argtype(Base.iterate)))

Output:

75-element Array{Any,1}:
 Core.SimpleVector                      
 ExponentialBackOff                     
 Base.Iterators.ProductIterator{Tuple{}}
 BitSet                                 
 String                                 
 Base.RegexMatchIterator                
 Base.EnvDict                           
 Cmd                                    
 Base.AsyncCollector                    
 Base.AsyncGenerator                    
 LibGit2.GitBranchIter                  
 LibGit2.GitConfigIter                  
 LibGit2.GitRevWalker                   
 ⋮                                      
 QR                                     
 LinearAlgebra.QRCompactWY              
 QRPivoted                              
 Hessenberg                             
 LQ                                     
 Union{Eigen, GeneralizedEigen}         
 SVD                                    
 GeneralizedSVD                         
 LU                                     
 BunchKaufman                           
 Schur                                  
 GeneralizedSchur                       

Notice however some of the types we found are from other modules – modules that are loaded but not in
a quick hack that lets use exclude those is to check if occursin(".", string(T)).

Input:

nonscalar_types = filter(nonscalar_types) do T
    !occursin(".", string(T)) #Quick hack to see if it is in a module that is loaded
end

Output:

34-element Array{Any,1}:
 ExponentialBackOff            
 BitSet                        
 String                        
 Cmd                           
 Tuple                         
 Pair                          
 Union{LinRange, StepRangeLen} 
 OrdinalRange                  
 Number                        
 Ref                           
 LinearIndices                 
 Array                         
 BitArray                      
 ⋮                             
 Channel                       
 QR                            
 QRPivoted                     
 Hessenberg                    
 LQ                            
 Union{Eigen, GeneralizedEigen}
 SVD                           
 GeneralizedSVD                
 LU                            
 BunchKaufman                  
 Schur                         
 GeneralizedSchur              

Once we have out types we need to generate the code.
The following is a pretty hacky way to do so, abusing strings.
But does give a nice example of dispatch again.

Input:

string_form(T) = string(T)
string_form(T::UnionAll) = string(T.body, " where ", T.var)


function scalarness_function_expr(T)
    str_T = string_form(T)
    parts = split(str_T, "where"; limit=2)
    scalarness_function_expr(parts...)
end


# 1 arg means no where clause
function scalarness_function_expr(type_str::AbstractString)
    type_expr = Meta.parse(type_str)
    
    :(scalarness(::Type{<:$(type_expr)}) = NonScalar())
end

# 2 arg means found a where clause
function scalarness_function_expr(type_str::AbstractString, where_str::AbstractString)
    type_expr = Meta.parse(type_str)
    Meta.parse(string(:(scalarness(::Type{<:$(type_expr)}))) * " where $(where_str) = NonScalar()")
end

Output:

scalarness_function_expr (generic function with 3 methods)

Input:

scalarness_function_expr(BitSet)

Output:

:(scalarness(::Type{<:BitSet}) = begin
          #= In[41]:16 =#
          NonScalar()
      end)

Input:

scalarness_function_expr(last(nonscalar_types))

Output:

:(((scalarness(::Type{<:GeneralizedSchur{Ty, M}}) where M <: (AbstractArray{T, 2} where T)) where Ty) = begin
          #= none:1 =#
          NonScalar()
      end)

Input:

scalarness_function_expr(nonscalar_types[6])

Output:

:(((scalarness(::Type{<:Pair{A, B}}) where B) where A) = begin
          #= none:1 =#
          NonScalar()
      end)

Looks like good code that we could evaluate.
I know it doesn’t capture all cases,
but it is more than enough for demonstration.

Input:

for T in nonscalar_types
    try
        eval(scalarness_function_expr(T))
    catch err
        println()
        @show err
        @show T
        @show scalarness_function_expr(T)
    end        
end

Output:

err = TypeError(:<:, "", Type, R<:Tuple{})
T = CartesianIndices{0,R} where R<:Tuple{}
scalarness_function_expr(T) = :((scalarness(::Type{<:CartesianIndices{0, R <: Tuple{}}}) where R <: Tuple{}) = begin
          #= none:1 =#
          NonScalar()
      end)

err = ErrorException("syntax: incomplete: premature end of input")
T = Union{KeySet{#s57,#s56} where #s56<:Dict where #s57, ValueIterator{#s55} where #s55<:Dict}
scalarness_function_expr(T) = :($(Expr(:incomplete, "incomplete: premature end of input")))

err = TypeError(:<:, "", Type, T<:AbstractString)
T = SubString
scalarness_function_expr(T) = :((scalarness(::Type{<:SubString{T <: AbstractString}}) where T <: AbstractString) = begin
          #= none:1 =#
          NonScalar()
      end)

err = ErrorException("syntax: invalid variable expression in \"where\"")
T = BunchKaufman
scalarness_function_expr(T) = :(((scalarness(::Type{<:BunchKaufman{T, S}}) where S <: (AbstractArray{T, 2} where T)) where T) = begin
          #= none:1 =#
          NonScalar()
      end)

So, this worked for most of them.
It would be possible to go back and tweak out metaprogramming to catch the last few.

Input:

methods(scalarness)

Output:

# 34 methods for generic function "scalarness":
[1] scalarness(::Type{#s11} where #s11<:Union{LinRange, StepRangeLen}) in Main at In[41]:16
[2] scalarness(::Type{#s11} where #s11<:OrdinalRange{T,S}) where {T, S} in Main at none:1
[3] scalarness(::Type{#s11} where #s11<:LinearIndices{N,R}) where {N, R<:Tuple{Vararg{AbstractUnitRange{Int64},N}}} in Main at none:1
[4] scalarness(::Type{#s11} where #s11<:Array{T,N}) where {T, N} in Main at none:1
[5] scalarness(::Type{#s11} where #s11<:BitArray{N}) where N in Main at none:1
[6] scalarness(::Type{#s11} where #s11<:CartesianIndices{N,R}) where {N, R<:Tuple{Vararg{AbstractUnitRange{Int64},N}}} in Main at none:1
[7] scalarness(::Type{#s11} where #s11<:AbstractArray{T,N}) where {T, N} in Main at none:1
[8] scalarness(::Type{#s1} where #s1<:AbstractArray) in Main at In[20]:2
[9] scalarness(::Type{#s11} where #s11<:String) in Main at In[41]:16
[10] scalarness(::Type{#s1} where #s1<:AbstractString) in Main at In[20]:4
[11] scalarness(::Type{#s11} where #s11<:BitSet) in Main at In[41]:16
[12] scalarness(::Type{#s11} where #s11<:AbstractSet) in Main at In[25]:1
[13] scalarness(::Type{#s11} where #s11<:ExponentialBackOff) in Main at In[41]:16
[14] scalarness(::Type{#s11} where #s11<:Cmd) in Main at In[41]:16
[15] scalarness(::Type{#s11} where #s11<:Tuple) in Main at In[41]:16
[16] scalarness(::Type{#s11} where #s11<:Pair{A,B}) where {A, B} in Main at none:1
[17] scalarness(::Type{#s11} where #s11<:Number) in Main at In[41]:16
[18] scalarness(::Type{#s11} where #s11<:Ref{T}) where T in Main at none:1
[19] scalarness(::Type{#s11} where #s11<:NamedTuple{names,T}) where {names, T<:Tuple} in Main at none:1
[20] scalarness(::Type{#s11} where #s11<:Dict{K,V}) where {K, V} in Main at none:1
[21] scalarness(::Type{#s11} where #s11<:AbstractChar) in Main at In[41]:16
[22] scalarness(::Type{#s11} where #s11<:CartesianIndex{N}) where N in Main at none:1
[23] scalarness(::Type{#s11} where #s11<:Channel{T}) where T in Main at none:1
[24] scalarness(::Type{#s11} where #s11<:QR{T,S}) where {T, S<:AbstractArray{T,2}} in Main at none:1
[25] scalarness(::Type{#s11} where #s11<:QRPivoted{T,S}) where {T, S<:AbstractArray{T,2}} in Main at none:1
[26] scalarness(::Type{#s11} where #s11<:Hessenberg{T,S}) where {T, S<:AbstractArray{T,2}} in Main at none:1
[27] scalarness(::Type{#s11} where #s11<:LQ{T,S}) where {T, S<:AbstractArray{T,2}} in Main at none:1
[28] scalarness(::Type{#s11} where #s11<:Union{Eigen, GeneralizedEigen}) in Main at In[41]:16
[29] scalarness(::Type{#s11} where #s11<:SVD{T,Tr,M}) where {T, Tr, M<:(AbstractArray{T,N} where N)} in Main at none:1
[30] scalarness(::Type{#s11} where #s11<:GeneralizedSVD{T,S}) where {T, S} in Main at none:1
[31] scalarness(::Type{#s11} where #s11<:LU{T,S}) where {T, S<:AbstractArray{T,2}} in Main at none:1
[32] scalarness(::Type{#s11} where #s11<:Schur{Ty,S}) where {Ty, S<:(AbstractArray{T,2} where T)} in Main at none:1
[33] scalarness(::Type{#s1} where #s1<:GeneralizedSchur{Ty,M}) where {Ty, M<:(AbstractArray{T,2} where T)} in Main at none:1
[34] scalarness(::Type{T}) where T in Main at In[27]:2

Input:

@show scalarness(Dict{Int,Int})
@show scalarness(typeof([1,3,2]'))

Output:

scalarness(Dict{Int, Int}) = NonScalar()
scalarness(typeof(([1, 3, 2])')) = NonScalar()
NonScalar()

Input:

@code_warntype scalarness(typeof([1,3,2]'))

Output:

Body::NonScalar
│1 1 ─     return $(QuoteNode(NonScalar()))

So we have done this,
and generated a ton of methods.
And we avoid doing a dynamic dispatch on hasmethod,
except as a fallback,
for types that were not loaded when our metaprogramming run.
A function could be exposed to rerun this.

The whole thing of metaprograming over reflection is kinda evil though.
But it is perhaps interesting to see how it can be done.