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, 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?
Compact Mode
Before I go to the solution, it turns out there is a "compact" mode to displaying objects. 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 more compact display, since you may have many objects. I often forget about this compact 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
# default show used by Array show
function Base.show(io::IO, obj::MyType)
compact = get(io, :compact, true)
print_object(io, obj, compact)
end
# default show used by display() on the REPL
function Base.show(io::IO, mime::MIME"text/plain", obj::MyType)
compact = get(io, :compact, false)
print_object(io, obj, compact)
end
function print_object(io::IO, obj::MyType, compact::Bool)
if compact
# write something short, or go back to default mode
Base.show_default(io, obj)
else
print(io, "MyType")
print(io, "\n ")
print(io, "some_number: $(obj.some_number)")
print(io, "\n ")
print(io, "some_dict: $(obj.some_dict)")
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 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.
Here's where I found stuff in the base language:
the Dict show called by the array.
the Array show internals.
If you read the code, please note the other IO options, like :limit
. Can you guess what it's used for?
Conclusion
Well that's it, hope it helps as a reference to you and future me ;)