User-defined Show Method in Julia
How do you best configure the display of custom type contents?
I often find myself looking for a way to write custom display methods for Julia types on the REPL. Time to write it down in a short pragmatic blog post, for you and my future self.
What's the issue? When exploring on the Julia REPL or in notebooks, you display your own custom type, then it doesn't look always look the most informative. Let's say you have some type:
struct MyType
some_number::Float64
some_dict::Dict
end
You can quickly make an object and display it.
julia> obj = MyType(4.0, Dict(:x => 5))
MyType(4.0, Dict(:x => 5))
Okay... Julia basically shows the constructor of the object. I would like to see the field names, or maybe other information. Sometimes I want to see statistical properties for example, instead of the raw data.
As an alternative, to quickly see the field names, you can dump
the content of an object. Which is nice for simple objects, but I explicitly put a Dict
in there to mess it up, because it'll dump the dictionary internals, which you don't want to see:
julia> dump(obj)
MyType
some_number: Float64 4.0
some_dict: Dict{Symbol, Int64}
slots: Array{UInt8}((16,)) UInt8[0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x82]
keys: Array{Symbol}((16,))
1: #undef
2: #undef
...
15: #undef
16: Symbol x
vals: Array{Int64}((16,)) [5065505441550857052, 465637893754, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 5]
ndel: Int64 0
count: Int64 1
age: UInt64 0x0000000000000001
idxfloor: Int64 16
maxprobe: Int64 0
Not pretty. How to improve this developer experience?
Switching display mode
Before I go to the solution, it turns out there are different "modes" of printing an object. You can notice this behavior when you place dictionaries inside an array for example:
julia> d = Dict(:a => 1, :b => 2, :c => 3)
Dict{Symbol, Int64} with 3 entries:
:a => 1
:b => 2
:c => 3
julia> [d, Dict(:d => 4)]
2-element Vector{Dict{Symbol, Int64}}:
Dict(:a => 1, :b => 2, :c => 3)
Dict(:d => 4)
You see that the dictionary is displayed differently in the two cases above. Inside the array we prefer a single line display, since you may have many objects. I sometimes forget to properly implement this shorter mode, and then I get ugly array printing.
Here's some discussion on the topic on the Julia discourse.
Custom show
In the end, this is a typical approach I take. You can make it a lot more fancy if you like, but this is a good starting point:
struct MyType
some_number::Float64
some_dict::Dict
end
# 2-argument show, used by Array show, print(obj) and repr(obj), keep it short
function Base.show(io::IO, obj::MyType)
print_object(io, obj, multiline = false)
end
# the 3-argument show used by display(obj) on the REPL
function Base.show(io::IO, mime::MIME"text/plain", obj::MyType)
# you can add IO options if you want
multiline = get(io, :multiline, true)
print_object(io, obj, multiline = multiline)
end
function print_object(io::IO, obj::MyType; multiline::Bool)
if multiline
print(io, "MyType") # or call summary(io, obj)
print(io, "\n ")
print(io, "some_number: $(obj.some_number)")
print(io, "\n ")
print(io, "some_dict: $(obj.some_dict)")
else
# write something short, or go back to default mode
Base.show_default(io, obj)
end
end
This works fine:
julia> t = MyType(5.0, Dict(:a => 1, :b => 2))
MyType
some_number: 5.0
some_dict: Dict(:a => 1, :b => 2)
julia> [t]
1-element Vector{MyType}:
MyType(5.0, Dict(:a => 1, :b => 2))
You can test the IO context options as follows:
julia> show(IOContext(stdout, :compact => true), MIME"text/plain"(), t)
MyType
some_number: 5.0
some_dict: Dict(:a=>1, :b=>2)
julia> show(IOContext(stdout, :compact => true, :multiline => false), MIME"text/plain"(), t)
MyType(5.0, Dict(:a=>1, :b=>2))
You can make your type printing as fancy as you desire.
One additional trick, to make the code more concise when you have a lot of properties with special types, you can also loop over the propertynames(obj)
and use for example getproperty(obj, :name)
. Now I hardcoded the property names in the example above, such as in the line print(io, "some_number: $(obj.some_number)")
.
Here's where I found stuff in the base language for inspiration:
the Dict show called by the array.
the Array show internals.
In these examples above I found out that there are commonly used IO options, like :compact
which are described in the Base.IOContext documentation. You can choose to implement such options in your custom show methods, to provide more user configuration to the printing.
Conclusion
Well that's it, hope it helps as a reference to you and future me ;)