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 begin struct $T <: Temperature value::Float64 end 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) end end # Promotion + conversion: for (_, T) in types, (_, S) in types (S != T) && begin $T(temp::$S) = convert($T, temp); end 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:
3°C
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 Base.show(io::IO, k::$T) = print(io, k.value, $(string(Short))) end # 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 Base.show(io::IO, ::MIME"text/html", k::Celsius) print(io, k.value, """<font color="red">°C</font>""") end
3.0°C
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 fill::Colorant stroke::Colorant strokewidth::Int radius::Int origin::NTuple{2, Float64} end function Base.show(io::IO, ::MIME"image/svg+xml", c::Circle) print(io, """ <svg version="1.1" baseProfile="full" width="700" height="300" xmlns="http://www.w3.org/2000/svg"> <circle cx="$(c.origin[1])" cy="$(c.origin[1])" r="$(c.radius)" stroke="#$(hex(c.stroke))" stroke-width="$(c.strokewidth)" fill="#$(hex(c.fill))" /> </svg> """) end 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.
Pkg.add("LightXML") 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.