`show` is too low-level

The printing of tables has been discussed recently, and every time I wonder how many different table-printing implementations, or tree-printing implementations are in the Julia ecosystem.

Here is how show works:

As a package developer, each new type I write should ideally implement a show method for each of these MIME types. Plus Atom, which sometimes needs special handling. And maybe Visual Studio…? That’s a lot of work! Even Base doesn’t bother; Base.Profile.print, and Base.whos are pure ASCII, even in IJulia.

What I would like would be to just say “render this type as a table, with numbers below 0 in red”, then Julia, or some mystery package, would handle all the different MIME types to the best of that MIME type’s capabilities. Something like

using VisualElements   # to be written!

struct FamilyTreeNode
    person_name
    children
end

function VisualElements.renderas(node::FamilyTreeNode)
    # Display FamilyTreeNode objects as trees, and
    # highlight the people with no children.
    node_name = (isempty(node.children) ? 
                 VisualElements.emphasize(node.person_name) : 
                 node.person_name)
    VisualElements.tree(node_name,
                        map(renderas, node.children))
end

In my ideal world, renderas would be called from the fallback show method in Base, but this isn’t strictly necessary. VisualElements could offer a macro that implements all the show methods for a particular type.

I’m sorry to say that I can’t make this happen, but with 1.0 nearing, I wanted to throw it out there in case it inspires someone to get the ball rolling. show is nice, but it’s too low-level for most use cases.

5 Likes

Would it help if there was an intermediate format, with show defined?

Eg

function show(io::IO, mime, node::FamilyTreeNode)
    r = construct_representation(node)
    show(io, mime, r)
end

This could be done as a library later on.

Isn’t this what Markdown.Table does already?

1 Like

Maybe? I couldn’t find any documentation for it. How would the above example be written with Markdown? One problem is that Base.show(io::IO, mime, node::FamilyTreeNode) = ... creates a method ambiguity.

Yes, that’s what I’m suggesting (my renderas is your construct_representation). We’d have construct_representation(x) = x by default in Base, then some visual representation package X.jl would define show methods for many different intermediate representations, and package developers/users would import X.jl and implement construct_representation(x::MyType) = <build some X.jl objects>

1 Like

Yes — I just didn’t know about it :smile:

Apparently all the elements are in place for using Markdown as an interim construct, then printing nicely:

julia> t = Markdown.Table([["my", "favorite", "numbers"],
                           string.([19, 42, 137])],
                          [:l, :c, :r])
Base.Markdown.Table(Array{Any,1}[Any["my", "favorite", "numbers"], Any["19", "42", "137"]], Symbol[:l, :c, :r])

julia> m = Markdown.MD(t)
my favorite numbers
–– –––––––– –––––––
19    42        137

julia> show(STDOUT, MIME"text/latex"(), m)
\begin{tabular}
{l | c | r}
my & favorite & numbers \\
\hline
19 & 42 & 137 \\
\end{tabular}

julia> show(STDOUT, MIME"text/html"(), m)
<div class="markdown"><table><tr><th>my</th><th>favorite</th><th>numbers</th></tr><tr><td>19</td><td>42</td><td>137</td></tr></table>
</div>

(PS: I didn’t know we had a plea tag, but I like it :wink:)

3 Likes

I tried to use the above to define show methods using Markdown as the interim representation:

import Base.Markdown

struct Foo
    x
end

function Base.show(io::IO, mime, f::Foo)
    t = Markdown.Table([[string(f.x)]], [:c])
    m = Markdown.MD(t)
    show(io, mime, m)
end

but I get

julia> Foo(1)
Error showing value of type Foo:
ERROR: MethodError: no method matching display(::Foo)
Closest candidates are:
  display(::Any) at multimedia.jl:191
  display(::Display, ::AbstractString, ::Any) at multimedia.jl:125
  display(::AbstractString, ::Any) at multimedia.jl:126
  ...
Stacktrace:
 [1] display(::Foo) at ./multimedia.jl:201
 [2] eval(::Module, ::Any) at ./boot.jl:235
 [3] print_response(::Base.Terminals.TTYTerminal, ::Any, ::Void, ::Bool, ::Bool, ::Void) at ./REPL.jl:144
 [4] print_response(::Base.REPL.LineEditREPL, ::Any, ::Void, ::Bool, ::Bool) at ./REPL.jl:129
 [5] (::Base.REPL.#do_respond#16{Bool,Base.REPL.##26#36{Base.REPL.LineEditREPL,Base.REPL.REPLHistoryProvider},Base.REPL.LineEditREPL,Base.LineEdit.Prompt})(::Base.LineEdit.MIState, ::Base.AbstractIOBuffer{Array{UInt8,1}}, ::Bool) at ./REPL.jl:646

am I doing this the wrong way?

That’s interesting; Markdown is irrelevant, you get the same error with an empty function body for show. Meanwhile, in IJulia, the same code yields a method ambiguity error.

This works in the terminal and Jupyter notebooks:

import Base: display, show
using Base.Markdown: @md_str, html

struct Fancy end

markdown_representation(::Fancy) = md"*emph* ``α``"

display(x::Fancy) = display(markdown_representation(x))

show(io::IO, ::MIME"text/html", x::Fancy) =
    println(io, html(markdown_representation(x)))

It is a pity that I have to hijack display, but could not find a workaround. Perhaps I should open an issue, defining a single method that just constructs a representation and prints everywhere would be nice.

You really only want to do this for MIME types where MD defines an output method, right? You could do:

for T in map(m -> m.sig.parameters[3], methods(show, (IO,MIME,Base.Markdown.MD)))
    @eval function Base.show(io::IO, mime::$T, f::Foo)
        t = Markdown.Table([[string(f.x)]], [:c])
        m = Markdown.MD(t)
        show(io, mime, m)
    end
end

to define pass-through methods for all MIME types supported by MD, which would also fix the ambiguity errors, right?

Hijacking display is a bad idea, not the least because it will only work for display calls, not for output to any other stream.

There is a method ambiguity because of the fallback show for text/plain (which calls the 2-argument show(io, x) by default). If you just define a text/plain method to resolve the ambiguity it works fine. (Currently, display prints method errors in a confusing way…would be good to fix this.)

The following works for me:

julia> struct Foo
           x
       end

julia> function myshow(io::IO, mime, f::Foo)
           t = Markdown.Table([[string(f.x)]], [:c])
           m = Markdown.MD(t)
           show(io, mime, m)
       end
myshow (generic function with 1 method)

julia> Base.show(io::IO, mime, f::Foo) = myshow(io, mime, f)

julia> Base.show(io::IO, mime::MIME"text/plain", f::Foo) = myshow(io, mime, f)

julia> Foo(3)
|  3  |
|:---:|

Probably you should also define

Base.mimewritable(mime::MIME, f::Foo) = mimewritable(mime, Markdown.md"")

This works (except for the last show definition, where you missed a mime), but ideally it would show the markdown rendered in the terminal. Currently the only way for this is to hijack display.

In any case, I think that the general point of this thread is that it would be great if one could define a method that renders a type into Markdown, and then relevant methods could display it in various media. This would just require a single method for universal pretty printing.

This is a related issue:
https://github.com/JuliaLang/julia/issues/10009

What do you mean? It is using the markdown display in the terminal, as shown in my output

julia> Foo(3)
|  3  |
|:---:|

That is, the REPL is using the Markdown.MD type’s text/plain output. Isn’t that what you want?

In any case, I think that the general point of this thread is that it would be great if one could define a method that renders a type into Markdown, and then relevant methods could display it in various media. This would just require a single method for universal pretty printing.

Isn’t that what my code does? Modulo an extra method to fix the ambiguity?

No, I want the nicely rendered output, even in the terminal. This is what it looks like for the example above with Fancy:
Screenshot_2017-12-13_19-58-39

Oh, I see the problem: the Base.Markdown module overrides Base.display(d::Base.REPL.REPLDisplay, md::MD). Probably it shouldn’t do this: it should use the IOContext properties in show to decide whether to use terminal formatting codes (in the same way that we should use these properties to decide whether to print ANSI color codes).

(I’m guessing that this code for Markdown display pre-dates the whole IOContext stuff.)

Update: fixed by #25067.

1 Like

This touches an idea I’ve had for a while now (excuse me for rambling):

Should we start thinking of recipes in other contexts?

Plots.jl’s implementation of recipes is powerful because with almost no overhead you can tell Plots.jl how to plot your custom types iff the user wants to. It’s like really functional and light weight glue. Another way of tackling this is adding some conditional code that connects your package with some other package with Requires.jl (but from what I’ve heard [from Tim holy] it’s not “precompile-friendly”).

I’ve seen multiple cases where the need to connect unique packages to a common output package exists. Instead of including all the possible outputs in the unique package, or vice versa, I keep thinking that a Recipes.jl would really do the trick.

Here are some examples to get you thinking:

  1. AxisArrays/OffsetArrays → CSV: offsets, axis values, and names can be parsed into a csv when saving.
  2. AxisArrays/OffsetArrays → Interpolations: the interpolation of an such an array should be automatically scaled/offset.
  3. CategorialArrays → HDF5: With the sparse format of these arrays, the HDF5 save should also reflect that.
  4. and all the show methods…

Ramble out.

6 Likes

The recipes do a lot more than that. Because (type) recipes are applied recursively until you get to some basic bottom types that have a representation, a (type) recipe just needs to output some type which may not have a representation. The DiffEq recipe says how a sol(t,u). The UnitfulPlots.jl recipe says how to strip the units. The DimensionalPlotRecipes.jl recipe says how to reduce complex numbers to multiple dimensions of floating point numbers. When someone does plot(sol) on a DiffEq solution where the state variables are complex numbers with units, then if the user has these packages (i.e. the correct apply_recipe dispatches), then the chain

sol -> (t,u) -> unitless (t,u) -> floating point (t,real(u),imaginary(u))

all by recusing the simple type handling dispatches which Plots.jl does until it gets something it can handle.

The power of this cannot be overblown. The “standard way” of extending plotting is to write functions. Someone in Python creates a number object with units and creates function that calls matplotlib internally. But this way of writing plotting functions doesn’t compose. DiffEq is able to write its conversion without having to know how the number type will convert, and the same for Unitful, etc.

The second part to Plots.jl is that it has series recipes which say how you should actually plot a primitive. So a bunch of arrays: how do you plot them? Bunch of lines? Scatter plot? These are the series recipes.

Having this in mind, a recipe system for show would be great! A lot of people would probably just implement a type recipe which converts their type to say a Markdown object, and then let’s that show do its work. @Tamas_Papp would implement a new series recipe for Markdown objects, and then have his type do a type recipe plus impose a downstream series choice. The show mechanism would have to pass along extra information about the downstream display (which it currently does, like whether it should be compact) that the recipes would use. The Markdown object isn’t the best example since it probably naturally recurses using a show to build the strings for inside of the cells, but there’s probably similarly complicated examples as the DiffEq plot which would apply here.

2 Likes

I am nowhere near that sophisticated :wink: Basically, I just want to minimize effort for implementing various output formats, and thought of going through Markdown (not the markup format in plain text, but the internal representation of the semantics) as a reasonable common way to do this.

Initially I thought that this should be in Base, now I think this is fine as a package. And Markdown may be too limiting.