Idea for better methods / dispatch visualization

I was wondering if anybody has thought about this before. Sometimes it can be hard to keep track of what methods are available for a given function and how they’re hierarchically organized. For example, the + function has around 200 methods in a fresh Julia session. So far, methods(+) will print a list of unordered methods. I thought, why not exploit the tree structure of the type signatures to group methods.

Here’s some preliminary code that does this and prints the structure via AbstractTrees.jl. Would something like this be useful to have by default in Julia (or is there already something I missed)?

using AbstractTrees

mutable struct Node
    sig::Type
    children::Vector{Node}
end

function Base.show(io::IO, n::Node)
    s = replace(string(n.sig), r"^Tuple" => "")
    print(io, s == "" ? "root" : s)
end

AbstractTrees.children(n::Node) = n.children

function Node(method)
    s = method.sig

    function skipfirst(x)
        if typeof(x) === UnionAll
            UnionAll(x.var, skipfirst(x.body))
        else
            Tuple{x.parameters[2:end]...}
        end
    end

    Node(skipfirst(s), [])
end

function add_to_tree!(node, x)
    @assert x.sig <: node.sig
    added_to_child = false
    for child in node.children
        if x.sig <: child.sig
            add_to_tree!(child, x)
            added_to_child = true
        end
    end
    if !added_to_child
        for i in length(node.children):-1:1
            child = node.children[i]
            if child.sig <: x.sig
                deleteat!(node.children, i)
                add_to_tree!(x, child)
            end
        end
        push!(node.children, x)
    end
    return node
end

function methodtree(func::Function)
    ms = methods(func)
    root = Node(Tuple{Vararg{Any}}, [])
    for m in ms
        add_to_tree!(root, Node(m))
    end
    return root
end

And here’s an example printout for push! just because it’s not such a wall of text as + is:

print_tree(methodtree(push!))

root
├─ {AbstractChannel, Any}
├─ {Base.IdSet, Any}
├─ {Base.InvasiveLinkedList{Base.LinkedListItem{T}}, T} where T
├─ {Base.InvasiveLinkedList{T}, T} where T
├─ {Base.InvasiveLinkedListSynchronized{T}, T} where T
├─ {OrderedCollections.OrderedSet, Any}
├─ {Set, Any}
├─ {Pkg.Resolve.ResolveLogEntry, Tuple{Union{Nothing, Pkg.Resolve.ResolveLogEntry}, String}}
├─ {LoweredCodeUtils.Links, Any}
├─ {Markdown.MD, Any}
├─ {Revise.PkgData, Pair{<:AbstractString, Revise.FileInfo}}
├─ {CompositeException, Any}
├─ {AbstractVector, Vararg{Any}}
│  ├─ {Vector{T}, Any} where T
│  │  └─ {Vector{Any}, Any}
│  ├─ {BitVector, Any}
│  └─ {VSCodeServer.JSON.Parser.PushVector, Any}
├─ {Distributed.AbstractWorkerPool, Int64}
│  └─ {Distributed.WorkerPool, Int64}
├─ {AbstractDict, Pair}
│  └─ {Base.EnvDict, Pair{<:AbstractString}}
├─ {BitSet, Vararg{Integer}}
│  └─ {BitSet, Integer}
├─ {Base.Nowhere, Any}
├─ {Revise.WatchList, Pair{<:AbstractString, Revise.PkgData}}
├─ {Revise.WatchList, Pair{<:AbstractString, CodeTracking.PkgFiles}}
├─ {Revise.WatchList, Pair{<:AbstractString, Base.PkgId}}
└─ {Any, Any, Any, Vararg{Any}}
   ├─ {Any, Any, Any}
   │  └─ {Pkg.Resolve.ResolveLogEntry, Tuple{Union{Nothing, Pkg.Resolve.ResolveLogEntry}, String}, Bool}
   └─ {AbstractDict, Pair, Pair, Vararg{Pair}}
      └─ {AbstractDict, Pair, Pair}
3 Likes

Is a tree the right thing? For multiple arguments I would expect +(::Int64, ::Int64) to appear below both +(::Int64, ::Number) and +(::Number, ::Int64), but that would be repetitive to represent in a tree.

Hmm interesting, maybe that’s true. I thought that each tuple type would have only one direct supertype without ambiguous methods, but maybe that’s not true. Then it would be a graph, not a tree.

They are sorted initially. The list is ordered from morespecific to least. So the first applicable method will be the one called (aside from ambiguities happening turning the tree into a graph)

3 Likes