Hide long types in error output for better readability

I am new to Julia and I like the language, but one thing I don’t like is the error messages. They are often a big block of mostly incomprehensible text which I suspect might scare some developers away. This is largely due to very complicated type definitions.
I’m not saying that we should remove information just format it nicer, maybe provide some aliases that can be expanded later.

Just look at this cutout of an error message I recently got (from Query.jl): (You could of course blame the developers for making so complicated types, but…)

DataValues.DataValueException()

Stacktrace:
DataValues.DataValueException()
[15] println(::Base.TTY, ::QueryOperators.EnumerableMap{NamedTuple{(:source, :timestamp, :val),Tuple{QueryOperators.GroupColumnArrayView{DataValues.DataValue{String},Grouping{Array{DataValues.DataValue{String},1},NamedTuple{(:timestamp, :market, :type, :psr_meaning, :quantity, :amount),Tuple{String,String,String,DataValues.DataValue{String},DataValues.DataValue{String},DataValues.DataValue{Float64}}}},:psr_meaning},QueryOperators.GroupColumnArrayView{String,Grouping{Array{DataValues.DataValue{String},1},NamedTuple{(:timestamp, :market, :type, :psr_meaning, :quantity, :amount),Tuple{String,String,String,DataValues.DataValue{String},DataValues.DataValue{String},DataValues.DataValue{Float64}}}},:timestamp},Int64}},QueryOperators.EnumerableIterable{Grouping{Array{DataValues.DataValue{String},1},NamedTuple{(:timestamp, :market, :type, :psr_meaning, :quantity, :amount),Tuple{String,String,String,DataValues.DataValue{String},DataValues.DataValue{String},DataValues.DataValue{Float64}}}},QueryOperators.EnumerableGroupBy{Grouping{Array{DataValues.DataValue{String},1},NamedTuple{(:timestamp, :market, :type, :psr_meaning, :quantity, :amount),Tuple{String,String,String,DataValues.DataValue{String},DataValues.DataValue{String},DataValues.DataValue{Float64}}}},Array{DataValues.DataValue{String},1},NamedTuple{(:timestamp, :market, :type, :psr_meaning, :quantity, :amount),Tuple{String,String,String,DataValues.DataValue{String},DataValues.DataValue{String},DataValues.DataValue{Float64}}},QueryOperators.EnumerableIterable{NamedTuple{(:timestamp, :market, :type, :psr_meaning, :quantity, :amount),Tuple{String,String,String,DataValues.DataValue{String},DataValues.DataValue{String},DataValues.DataValue{Float64}}},Tables.DataValueRowIterator{NamedTuple{(:timestamp, :market, :type, :psr_meaning, :quantity, :amount),Tuple{String,String,String,DataValues.DataValue{String},DataValues.DataValue{String},DataValues.DataValue{Float64}}},Tables.RowIterator{NamedTuple{(:timestamp, :market, :type, :psr_meaning, :quantity, :amount),Tuple{Array{String,1},Array{String,1},Array{String,1},Array{Union{Missing, String},1},Array{Union{Missing, String},1},Array{Union{Missing, Float64},1}}}}}},var"#451#461",var"#452#462"}},var"#454#464"{var"#448#458"}}) at ./strings/io.jl:73
 [16] println(::QueryOperators.EnumerableMap{NamedTuple{(:source, :timestamp, :val),Tuple{QueryOperators.GroupColumnArrayView{DataValues.DataValue{String},Grouping{Array{DataValues.DataValue{String},1},NamedTuple{(:timestamp, :market, :type, :psr_meaning, :quantity, :amount),Tuple{String,String,String,DataValues.DataValue{String},DataValues.DataValue{String},DataValues.DataValue{Float64}}}},:psr_meaning},QueryOperators.GroupColumnArrayView{String,Grouping{Array{DataValues.DataValue{String},1},NamedTuple{(:timestamp, :market, :type, :psr_meaning, :quantity, :amount),Tuple{String,String,String,DataValues.DataValue{String},DataValues.DataValue{String},DataValues.DataValue{Float64}}}},:timestamp},Int64}},QueryOperators.EnumerableIterable{Grouping{Array{DataValues.DataValue{String},1},NamedTuple{(:timestamp, :market, :type, :psr_meaning, :quantity, :amount),Tuple{String,String,String,DataValues.DataValue{String},DataValues.DataValue{String},DataValues.DataValue{Float64}}}},QueryOperators.EnumerableGroupBy{Grouping{Array{DataValues.DataValue{String},1},NamedTuple{(:timestamp, :market, :type, :psr_meaning, :quantity, :amount),Tuple{String,String,String,DataValues.DataValue{String},DataValues.DataValue{String},DataValues.DataValue{Float64}}}},Array{DataValues.DataValue{String},1},NamedTuple{(:timestamp, :market, :type, :psr_meaning, :quantity, :amount),Tuple{String,String,String,DataValues.DataValue{String},DataValues.DataValue{String},DataValues.DataValue{Float64}}},QueryOperators.EnumerableIterable{NamedTuple{(:timestamp, :market, :type, :psr_meaning, :quantity, :amount),Tuple{String,String,String,DataValues.DataValue{String},DataValues.DataValue{String},DataValues.DataValue{Float64}}},Tables.DataValueRowIterator{NamedTuple{(:timestamp, :market, :type, :psr_meaning, :quantity, :amount),Tuple{String,String,String,DataValues.DataValue{String},DataValues.DataValue{String},DataValues.DataValue{Float64}}},Tables.RowIterator{NamedTuple{(:timestamp, :market, :type, :psr_meaning, :quantity, :amount),Tuple{Array{String,1},Array{String,1},Array{String,1},Array{Union{Missing, String},1},Array{Union{Missing, String},1},Array{Union{Missing, Float64},1}}}}}},var"#451#461",var"#452#462"}},var"#454#464"{var"#448#458"}}) at ./coreio.jl:4

Inside a small terminal window this is hopeless.

So maybe we could use something like BigLongTypeAlias (automatically) and then expand this at some other place like a type tree/graph. Also there could be a function in the repl that would collapse/hide these long types.

Does anyone agree? I think this can be a huge improvement to the Julia user experience.

(Please excuse me if this has been a topic before, but I couldn’t find anything.)

2 Likes

There have been some mentions of this before. I remember reading them, but Discourse’s search engine isn’t always the best…

Here’s one I found though: https://discourse.julialang.org/t/pretty-print-of-type/19555 It at least describes how this might be fixed for individual types, though the fix is not exactly “generally recommended”.

Some packages are worse than others — Query.jls are famously Byzantine. Sometimes I think a little expandable tree would be great, to save some scrolling.

3 Likes

For any one type I think you can overload the relevant show method, and print something abbreviated. Tracker.jl does something like this:

julia> param(rand(3)) |> typeof
TrackedArray{…,Array{Float64,1}}

Stack traces contain a lot of useful information. While it is natural that understanding them requires some familiarity with the language, they are invaluable for debugging and getting help — you can just copy and paste them, and convey a ton of useful information.

Because of this, I would suggest that you just ignore the details until you are more familiar with Julia, and gradually develop the skills to interpret stack traces.

You should perhaps use Julia inside some programming editor that helps with navigation. Eg in Emacs I find it very easy to navigate/highlight matching {}s and similar in REPL output. I suspect other IDEs have similar features.

The occasionally complex nested parametric types just reflect the complexity of Julia’s parametric type system. Combined with multiple dispatch and AOT compilation, this is one of the key ingredients that make Julia the powerful language it is.

Ignoring is possible, but it ties up a substantial part of the processing capacity of the brain (it’s no joke). At that, in most case most of the stack traces information is useless for me. Say, I forgot an argument of a function. A message like “function F requires at least 2 arguments” would be instantely helpful. The message citing a dosen or so “closest matches” and listing the stack trace to the deep internals of Julia is also helpful: After I stared for a while onto it and figured out what happened.

There was a discussion on Julia error messages somewhere on the GitHub, can’t find it now - anybody has a link?

Do you mean: https://github.com/cmcaine/julia-error-message-catalogue
?

1 Like

The fact is that it isn’t that simple. Sometimes the function F has a method with two arguments, but different types than those used. And you may also have dozens of two-argument methods, all of them with different types.

2 Likes

Probably this one:

https://github.com/JuliaLang/julia/issues/33065

The discussion contains a lot of interesting ideas. I don’t know the progress towards implementation.

1 Like

As @giordano pointed out, generally this is not something the language can figure out from the method table, because it is a design decision only apparent to the programmer who wrote the API. Eg

f(a, b, c) = :three
f(a) = :one
f(a, 1) # too few or too many?

This is of course further complicated by parametric types.

Also, f(a, b, args...)

1 Like

Yes, that’s it, thank you. Possibly I have also seen some useful discussion on the topic somewhere else, but not sure.

The discussion at GitHub - Stacktrace gripes is probably too technical for me, and I don’t have a GitHub account, so let me put my consideration here.

Yes, full stack trace convey a ton of potentially useful information. If you (@giordano , @Tamas_Papp ) probably spend most of your (work) time programming in Julia, if you don’t make any silly errors, and if you trained your eyes to discard unimportant lines even before they reach your brain so to say – then full stack trace could be perfect for you. Even than, I don’t think the information about the “Closest candidates” is what you need in most cases.

Now that’s we, who learn and would like to (maybe, only occasionally) use Julia. That’s me, who tend to make a lot of silly and obvious errors. Well, I am now sufficiently intelligent to scroll the stack trace and pinpoint the line number in my script which triggered the error and then often instantely know what happend. It is not THAT difficult, mostly it’s just boring and tiring and time consuming, and - let me cite @tim.holy , who has

… definitely seen folks be turned off by the complexity of our stacktraces

So I think the following could be done improve the user-frendliness for us without sacrifying the usefullness for you (at least some of these ideas were mentioned in some form in the cited GitHub discussion or somewhere else):

Set the verbosity level in a Julia variable, say 1 to 3. 3 would correspond to the current stack trace, 1 shows some minimal information. For function calls that would be the wrong number of arguments, if there are no methods with exactly that args number, otherwise something like no methods exist for foo({Int64}, {Bool})

Save the full error information in a (structured) Julia object, available from the REPL. If you are interested in the “Closest candidates” - type in >errorlog.last.closest and get it. Want to see the packages involved - - type >errorlog.last.packages

Yes, if you want to see in detail what happened before, call e.g. >errorlog[end-2].fullstack

As an option, write all that into a log file (could be useful for standalone Julia).

For the verbosity level of 1, show the only the stack traces through the “user” files. These could be defined as files in the directory of the main script, or let (the advanced) user define the folders and packages which they want to see in the short version of the stack trace.

Am I very naive?

1 Like

Agreed. A choice of verbosity level (or similar) would probably help beginners and veterans alike.

It is recognized that stack traces are quite complex and need to be improved (eg the issue you linked). This is work in progress.

However, the solution may not be as simple as you seem to imagine. With parametric multiple dispatch, selecting the “useful” part of the information can be difficult. This is what those discussions are about.

Also, I think that dividing the Julia community to “us” (casual users) and “them” (users you imagine to be experts) is not constructive here. As implemented now, stack traces contain both useful and less useful information for everyone. None of us relish scrolling through a lot of text, and we would all like to improve the signal/noise ratio. It is just not an easy problem.

3 Likes

Agreed. Nim has three choices of verbosity and I seem to remember the docs saying that the highest level is “only useful to developers”.

:100:

Agreed. I don’t think that the problem is technically infeasible, it is just hard. It is a good thing that Julians are greedy.

2 Likes

We could also work on making what we have easier to read. Simply making different kinds of information color coded would go a long way - you could look for blocks of color instead of actual characters. Especially helpful when you are scrolling through long messages.

Not sure how easy it is to do in practice but seems like low hanging fruit.

1 Like

I am sure PRs would be welcome.

There are two parts of long type strings:

  • Deeply nestred parametric types (with a lot of {} braces).
  • Long type names for each individual type (like QueryOperators.GroupColumnArrayView).

For some standard types like arrays or tuples there was a proposal to change their names to a shorter syntax in declarations: