Simon Danisch / Jan 07 2019

Julia's Display System

Julia offers a sophisticated display system with rich MIME types and different ways to hook up your types or your own display.

One appealing use case for using custom display of a custom type are units. I adapted the code from this excellent article (a must read for anyone interested in Julia coming from Python), which is a small, but functional unit system for temperatures.

abstract type Temperature end
types = (:(°C) => :Celsius, :K => :Kelvin, :(°F) => :Fahrenheit)
for (Short, T) in types
  @eval begin
    struct $T <: Temperature
    Base.:(+)(x::$T, y::$T) = $T(x.value + y.value)
    Base.:(-)(x::$T, y::$T) = $T(x.value - y.value)
    Base.:(*)(x::Number, y::$T) = $T(x*y.value)
    const $Short = $T(1)
# Promotion + conversion:
for (_, T) in types, (_, S) in types
  (S != T) && @eval begin $T(temp::$S) = convert($T, temp); end
promote_rule(::Type{Kelvin}, ::Type{Celsius})     = Kelvin
promote_rule(::Type{Fahrenheit}, ::Type{Celsius}) = Celsius
promote_rule(::Type{Fahrenheit}, ::Type{Kelvin})  = Kelvin
convert(::Type{T}, t::Temperature) where T = T(Celsius(t))
convert(::Type{Kelvin},  t::Celsius)     = Kelvin(t.value + 273.15)
convert(::Type{Celsius}, t::Kelvin)      = Celsius(t.value - 273.15)
convert(::Type{Celsius}, t::Fahrenheit)  = Celsius((t.value - 32)*5/9)
convert(::Type{Fahrenheit}, t::Celsius)  = Fahrenheit(t.value*9/5 + 32)
+(x::Temperature, y::Temperature) = +(promote(x,y)...)
-(x::Temperature, y::Temperature) = -(promote(x,y)...);

Let's display it like it is:


This is not a temperature, it's a confusing mess!! ;)

This calls for giving the temperature a custom representation when being displayed:

for (Short, T) in types
	@eval, k::$T) = print(io, k.value, $(string(Short)))
# Nextjournal tries to display any Julia object as JSON first 
# to also serialize the data.
# let's disable that for now, to see the real Julia display system at work:
# (more about showable later)
Base.showable(disp::MIME"application/json", ::Any) = false 
3°C # and zooop, beautiful display!

This also works recursively as expected:

[3°C, 4K]

Now this isn't using any mime type yet. Since show(io, instance) is equivalent to defining show(io, ::MIME"text/plain", instance), we can also overload how display our object for other mime types!

function, ::MIME"text/html", k::Celsius)
  print(io, k.value, """<font color="red">°C</font>""")

Now the nice thing is, that we didn't need to define anything else to automatically upgrade to the richer MIME type when applicable. This is because the display system uses showable to find the richest mime type, which is defined this way:

# check if a method show(::IO, MIME, ::Type) exists - if yes we use it!
showable(::MIME{mime}, x) where mime = hasmethod(show, Tuple{IO, MIME{mime}, typeof(x)})

This can be used to create much more powerfull graphical representations of your type:

using Colors
struct Circle
  origin::NTuple{2, Float64}

function, ::MIME"image/svg+xml", c::Circle)
  print(io, """
  <svg version="1.1"
     width="700" height="300"
    <circle cx="$(c.origin[1])" cy="$(c.origin[1])" r="$(c.radius)" stroke="#$(hex(c.stroke))" stroke-width="$(c.strokewidth)" fill="#$(hex(c.fill))" />
circle = Circle(colorant"red", colorant"black", 3, 100, (150, 150))

This can be arbitrarily extended to create a whole drawing system with rich output and fallbacks for e.g. a terminal (which e.g. Luxor has done).

The beauty of this approach is, that it works in all IDE's / Display systems that support rich mime types, e.g. Atom, IJulia and Nextjournal! It's what makes all Julia plotting packages work with rich output on any platform, since the display system doesn't need to know about any specific package and things just work™.

This isn't only important for beauty, but is an essential tool to display types, that would usually not be displayable or would only display garbage. Let's take a look at the LightXML package, which is a wrapper around a C library.

using LightXML
xml = parse_string(sprint(io-> show(io, MIME"image/svg+xml"(), circle)));
# this is the nice representation that LightXML gives us
show(xml) # show(xml) -> show(stdout, xml)
# force to call show's fallback implementation, that would get called if there
# wasn't an overload:
invoke(show, Tuple{typeof(stdout), Any}, stdout, xml)

The above is of course utterly uninformative and also wouldn't serialize, since the pointers will be invalid.