User-defined Show Method in Julia

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:

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 ;)