PrettyPrinting.jl icon indicating copy to clipboard operation
PrettyPrinting.jl copied to clipboard

Avoiding stack overflows

Open cscherrer opened this issue 2 years ago • 2 comments

Hi, thanks for the very nice package :)

I'd like to use PrettyPrinting in MeasureBase. To avoid having to define so many show methods, I'm doing this:

abstract type AbstractMeasure end
Base.show(io::IO, m::AbstractMeasure) = pprint(io, m)

What I'd like is to the default method the same as Base.show would be without PrettyPrinting, and allow users to add tile or quoteof methods for specialization. But with the current setup a new method ends up with

julia> struct MyMeasure <: AbstractMeasure end

julia> MyMeasure()
Error showing value of type MyMeasure:
ERROR: StackOverflowError:
Stacktrace:
     [1] IOBuffer(; read::Bool, write::Bool, append::Nothing, truncate::Bool, maxsize::Int64, sizehint::Int64)
       @ Base ./iobuffer.jl:105
     [2] sprint(f::Function, args::MyMeasure; context::Nothing, sizehint::Int64)
       @ Base ./strings/io.jl:108
     [3] #repr#429
       @ ./strings/io.jl:282 [inlined]
     [4] repr
       @ ./strings/io.jl:282 [inlined]
     [5] tile_repr
       @ ~/.julia/packages/PrettyPrinting/TjoQG/src/tile.jl:171 [inlined]
     [6] tile_expr_or_repr (repeats 2 times)
       @ ~/.julia/packages/PrettyPrinting/TjoQG/src/expr.jl:116 [inlined]
     [7] tile
       @ ~/.julia/packages/PrettyPrinting/TjoQG/src/tile.jl:174 [inlined]
     [8] pprint
       @ ~/.julia/packages/PrettyPrinting/TjoQG/src/PrettyPrinting.jl:35 [inlined]
     [9] show(io::IOBuffer, m::MyMeasure)
       @ Main ./REPL[3]:1
    [10] sprint(f::Function, args::MyMeasure; context::Nothing, sizehint::Int64)
       @ Base ./strings/io.jl:114
--- the last 8 lines are repeated 8645 more times ---

I think what I need is a quoteof or tile method that gives the same output as the default. But it's not clear to me how to get to this. This seems like it would be a common pattern. But maybe I'm missing something. Any suggestions?

cscherrer avatar Dec 04 '21 00:12 cscherrer

This is indeed an interesting problem. It is possible to work around this issue, although it is a bit challenging to accommodate to every edge case.

The default tiling function is called PrettyPrinting.tile_repl(), and it indirectly calls Base.show(). You could override this method for AbstractMeasure in order to break the infinite recursion:

using PrettyPrinting
using MeasureBase: AbstractMeasure
PrettyPrinting.tile_repr(m::AbstractMeasure) =
    PrettyPrinting.literal(sprint(Base.show_default, m))

Now you will get the expected output:

struct MyMeasure <: AbstractMeasure
end
println(MyMeasure()) # prints "MyMeasure()"
println(MyMeasure() ^ 2) # prints "MyMeasure() ^ 2"

The user can provide a custom representation for a new measure type by overriding PrettyPrinting.tile() or PrettyPrinting.quoteof():

PrettyPrinting.quoteof(m::MyMeasure) = :μμ
println(MyMeasure()) # prints "μμ"
println(MyMeasure() ^ 2) # prints "μμ ^ 2"

Unfortunately, it fails if the user, instead of defining tile() or quoteof(), directly overrides Base.show(). In such a case, the custom representation will be ignored by measure combinators:

Base.show(io::IO, m::MyMeasure) = print(io, "μμ")
println(MyMeasure()) # prints `μμ` as expected.
println(MyMeasure() ^ 2) # prints `MyMeasure() ^ 2`!

One way to fix this is to make tile_repr() call Base.show_default() only if there is no specialized show() for the given measure type:

PrettyPrinting.tile_repr(m::M) where {M <: AbstractMeasure} =
    let default = isempty(filter(m -> m.sig != Tuple{typeof(show), IO, AbstractMeasure},
                                 methods(show, Tuple{IO, M})))
        PrettyPrinting.literal(sprint(default ? Base.show_default : show, m))
    end

Alternatively, you could make Base.show(::IO, ::AbstractMeasure) call pprint() only if there is a custom implementation of PrettyPrinting.tile() or PrettyPrinting.quoteof():

Base.show(io::IO, m::AbstractMeasure) =
    has_custom_method(PrettyPrinting.tile, m) || has_custom_method(PrettyPrinting.quoteof, m) ?
        pprint(io, m) : Base.show_default(io, m)
has_custom_method(@nospecialize(f::F), @nospecialize(m::M)) where {F, M} =
    !isempty(filter(m -> m.sig != Tuple{F, Any}, methods(f, Tuple{M})))

Of course, such use of the reflection API is not to everyone's taste.

Hope this helps. Perhaps there is a way to improve PrettyPrinting API so that this problem does not occur, but the solution eludes me.

xitology avatar Dec 06 '21 07:12 xitology

Thanks @xitology for the detailed help. I've seen this problem come up in other contexts -- gracefully exiting a would-be infinite recursion. Sometimes you can play tricks, like passing around "countdown" and jumping out when it hits zero. But that's trickier when external calls are involved.

My quick fix just after asking this was to add

function Pretty.tile(d::M) where {M<:AbstractMeasure}
    the_names = fieldnames(typeof(d))
    result = Pretty.literal(repr(M))
    isempty(the_names) && return result * Pretty.literal("()")
    Pretty.list_layout(Pretty.tile.([getfield(d, n) for n in the_names]); prefix=result)
end

This tries to fake the default Base.show. I don't think this will be the final solution. But since it looks like there's no single solution, I'd like to get some feedback from others involved with MeasureTheory development before finalizing anything. Your response will be helpful for us to weigh these possibilities.

I don't know if it will be a day, week, or month before we get back to this. I'll leave it to you whether to close this issue, always easy to reopen if we have more questions.

Thanks again for your help :smile:

cscherrer avatar Dec 06 '21 17:12 cscherrer