Julia’s type system is awesome, but sometimes the types we encountered are very large and can be just too hard to read. That’s why I made this simple package SimpleTypePrint.jl to let you customize how types are displayed. Currently, it supports the following changes compared to Base.show(::Type):
Merging nested where clauses
Limiting the maximal type depth to dispaly
Displaying type names without module prefixes
Renaming type variables with conflicting names
Hope you find this useful && any feedback is welcome!
The nested where clause one is something we definitely should have in Base. Would you be willing to write a PR? Conflicting names might also belong, but I’m less certain.
That’s a concern that I also had! That’s why overriding Base.show is optional—SimpleTypePrint will only do so if you call the provided config_type_display function. You can also just manually call the show_type function without modifying Base.show.
Also as pointed out by @Tamas_Papp, calling config_type_display refines show for all types instead of a specific one, so its behavior should be more consistent (and hence safer?) than overriding show for some specific types. My understanding is that this can still potentially break other libraries if they depend on the specific behavior of the current Base.show implementation (e.g., show where clauses separately). But so far I haven’t encountered such cases yet (My limited experience: I have been using config_type_display with 30ish other libraries inside VSCode for the past month).
I am wondering if there is a non-pirating way to do these kind of things though — maybe using the display stack with a wrapper type?
Here is a quick proof of concept (which needs some extra work though):
struct MyIO <: IO
io::IO
end
function Base.show(io::MyIO, M::MIME"text/plain", ::Type{T}) where T
print(io.io, "custom $T")
end
Base.show(io::MyIO, M::MIME"text/plain", x) = show(io.io, M, x)
pushdisplay(TextDisplay(MyIO(stdout)))
Interesting idea! I tried this and it seems to be working for types printed to stdout. (BTW, for print to work, we will also need to define Base.write(s::SimpleTypeIO, x::UInt8) = write(s.io, x).) However, one main usage of this for me is to have simpler types shown in stacktraces. Any idea of how to make this work for stacktraces without pirating?
For stack traces, there is a function print_type_stacktrace which handles the colour printing of types. Overloading that to shorten things doesn’t seem likely to break anything else, and might be worth doing in general. Also discussed a bit in this thread. (Edit – maybe this is the issue: https://github.com/JuliaLang/julia/issues/36517 .)
I don’t think that would affect method error printing. It would be nice if that could be abbreviated to enough levels to show the mismatch, somehow. If f only accepts some particular kind of AbstractArray, and I call it with a tuple (of complicated objects), then the mismatch is at the outermost layer.
I like this idea. It sounds like to implement this, we will need a way to compute the (minimal?) differences between two given types. Intellij IDEA actually does this for Scala type mismatch. Is there a similar functionality already implemented in Julia? I imagine it could be tricky to implement this for the general cases since Julia supports union/intersection types with constraints on type parameters.
Agree it seems tricky, I don’t know of such a function, but it may exist.
I guess the easy part would be to trim the types of arguments which do match. In your readme’s example the first argument is already quite long, but since it matches, we probably don’t need to see all the detail.
Hmm… Would always trimming matched arguments to the minimal level be too aggressive and accidentally hide important information from the user? For example, that might lead to errors like
no methods match +(Array{...}, Float64),
where the compiler displays the closest candidate as +(Array{...}, Int), but the true source of the error is actually caused by the first argument having the incorrect type Array{Float64}.
Right, one would need to think about this carefully. If there are several partial matches, then it tries to display a few, and presumably they should all constrain the level of detail. I don’t know the list of partial matches is generated, nor how it gets trimmed for printing:
julia> f(x::Array{Int}, y::Int) = "Int"; f(x::Array{Float64}, y::Float64) = "Floats";
julia> f([1,2,3], 4.5)
ERROR: MethodError: no method matching f(::Vector{Int64}, ::Float64)
Closest candidates are:
f(::Array{Int64, N} where N, ::Int64) at REPL[41]:1
f(::Array{Float64, N} where N, ::Float64) at REPL[41]:1
julia> [1,2,3] + 4.5
ERROR: MethodError: no method matching +(::Vector{Int64}, ::Float64)
For element-wise addition, use broadcasting with dot syntax: array .+ scalar
Closest candidates are:
+(::Any, ::Any, ::Any, ::Any...) at operators.jl:597
+(::Base.TwicePrecision, ::Number) at twiceprecision.jl:267
+(::Array, ::Array...) at arraymath.jl:43
...
Yeah, I agree. I think it’s safer to use type difference to compute the minimal display level that is needed to highlight the type mismatch, but determining the maximal display level (that is as small as possible) can be much trickier.