| name | base-show |
| description | Add concise Base.show and Base.summary methods to Julia types whose default REPL representation is unhelpful or overwhelming. Use this skill whenever the user mentions that a type prints badly in the REPL, asks to improve how an object is displayed or printed, wants a custom show, summary, or repr for a Julia type, or says the REPL output is noisy, verbose, or hard to read. Also trigger when the user asks to "make the REPL output nicer", "add a show method", "add a summary method", "customize display", or "fix what prints when I type a variable name". This skill produces compact, informative Base.show and Base.summary methods and matching unit tests — invoke it proactively whenever show, summary, display, print, repr, or REPL output is mentioned in a Julia context.
|
base-show
Add concise Base.show(io::IO, ::MIME"text/plain", x::T) and Base.summary(io::IO, x::T) methods to Julia types whose default REPL representation is unhelpful or
overwhelming. Julia's default show dumps every field recursively; types that hold
DataFrames, large dictionaries, nested arrays, or many scalar fields produce screens
of unreadable text at the REPL.
Base.show(io, MIME"text/plain", x) must also handle the :compact IOContext key.
When Julia renders an object as an element inside a container (e.g. printing a
Vector{MyType}), it sets :compact => true on io. Without a compact branch the
full multi-line output is repeated for every element, producing an unreadable wall of
text. The compact branch must produce exactly one line (no newlines), giving the same
kind of at-a-glance hint as Base.summary.
This skill produces both methods and accompanying unit tests so that interactive use
of the package is pleasant without losing key summary information.
Workflow
Step 0 — Audit existing show methods (retrofit mode)
Skip this step if you are adding show methods to types that have none. Apply it when
the user asks to retrofit existing show methods — e.g. to add the compact branch to
methods that were written before this protocol existed.
Find MIME methods that lack the compact branch:
grep -n 'MIME"text/plain"' src/show.jl
For each match, check whether the function body contains get(io, :compact. Any that
do not are candidates for retrofit.
Detect the old forwarding anti-pattern (infinite-recursion risk):
grep -nA2 'function Base\.show(io::IO, x::' src/ | grep 'show(io, MIME'
If this matches, a 2-arg show(io, x) is calling the MIME method — the wrong
direction. Once the MIME method gains a compact branch that calls show(io, x), you
get infinite recursion. Flag every match and reverse the direction: the 2-arg method
becomes the compact one-liner, and the MIME method calls it via show(io, x) in its
compact branch.
Identify pre-existing bespoke 2-arg shows:
A bespoke 2-arg show is one that already exists but does not follow summary style —
for example, it may omit the type name entirely or use a different format. Check each
existing Base.show(io::IO, x::T) against its paired Base.summary. If the outputs
differ substantially, the 2-arg show is bespoke and needs a custom compact test (see
Step 4).
Step 1 — Enumerate concrete types
List every concrete (non-abstract) struct defined in the package source:
grep -nrE '^(mutable )?struct ' src/
Exclude abstract type declarations — they cannot be instantiated and do not need
show methods.
Step 2 — Classify show noisiness
For each concrete type, decide whether its default show output would be noisy. A type
is noisy if it holds at least one of:
- A
DataFrame or similar tabular collection
- A
Dict with potentially many entries
- A large or variable-length
Array
- Another struct that is itself noisy
- More than approximately six fields in total
Also run:
grep -nrE 'Base\.(show|summary)' src/
Skip any type that already has a custom Base.show or Base.summary method — do not
overwrite existing customization.
Step 3 — Write show and summary methods
For each noisy type without existing methods, write both a Base.show and a
Base.summary method.
Base.show — always write two overloads together:
# 3-arg MIME method: full REPL display, with compact fallback
function Base.show(io::IO, ::MIME"text/plain", x::T)
if get(io, :compact, false)
show(io, x) # delegate to the 2-arg compact method
else
println(io, "T")
println(io, " field_name : ", summary_value)
# ...
end
end
# 2-arg method: single-line compact representation (no newline)
function Base.show(io::IO, x::T)
print(io, "T (key_hint)")
end
The 3-arg (MIME) non-compact branch must:
- Print the type name (and any cheap size hints) on the first line.
- Follow with 1–5 concise summary lines: counts, sizes, or ranges of important fields.
Never print collection contents.
- Produce at most 10 lines of output for any valid instance, including edge cases such
as empty collections or zero-element structs.
The 2-arg method (compact representation) must:
- Produce exactly one line with no trailing newline.
- Match
Base.summary style: type name followed by the most essential identifying hint
in parentheses — e.g. "T (N_ens=100, 5 iter)".
- Remain O(1): no loops, no collection materialisation.
Julia calls the 2-arg method when rendering elements inside containers (arrays, dicts,
etc.), passing io with :compact => true. The MIME method's compact branch delegates
to it so both paths produce the same single-line output.
Base.summary — single-line description used when the object appears inside a
container or is printed in a broader context (e.g., as an element of a Vector):
function Base.summary(io::IO, x::T)
print(io, "T (key_hint)")
end
The method must:
- Fit on one line — no newlines.
- Convey the most important size or identity hint (e.g., number of elements, key
dimension), so the reader immediately knows what they are looking at.
- Remain cheap: O(1) field accesses only.
Good examples of what to put in the hint: "847 basins", "1000×365 grid",
"empty". Avoid repeating the type name verbatim as the only content — add value.
Placement: place both methods adjacent to their type definition in the same source
file, or gather all show/summary methods in a dedicated src/show.jl included from
the main module file. Follow whatever convention is already present in the package;
default to src/show.jl if no prior convention exists.
If creating src/show.jl, add include("show.jl") to the main module file after the
type definitions it references.
Step 4 — Write unit tests
Write one test block per type, covering show (full and compact), and summary. Each
test block must:
- Construct a minimal valid instance of the type.
- For full show: capture output with
sprint(show, MIME("text/plain"), instance) and
assert that it contains the type name and that line count does not exceed 10.
- For compact show: capture
out2 = sprint(show, instance) (2-arg) and assert it
contains the type name and has no '\n'. Also capture
out3 = sprint(show, MIME("text/plain"), instance; context=:compact => true) and
assert out2 == out3 — both compact paths must agree.
- For
summary: capture output with sprint(summary, instance) and assert that it
contains the type name and produces exactly one line (no '\n' in output).
Bespoke 2-arg shows (retrofit case): Some types may already have a 2-arg show
that intentionally does not include the type name or follow summary style — the method
is doing something custom. Using a shared check_compact(x, typename) helper will
fail the typename assertion for these. Instead, write a hand-rolled compact test:
s2 = sprint(show, instance)
@test !occursin('\n', s2) # no newline
@test s2 == sprint(show, MIME("text/plain"), instance; context = :compact => true) # paths agree
Avoid asserting exact strings so that cosmetic changes to the output do not break tests.
Step 5 — Verify
Run the package test suite:
julia --project -e 'using Pkg; Pkg.test()'
Confirm that all new tests pass and no pre-existing tests regress.
Step 6 — Offer to improve the skill
After the tests pass and the REPL output looks good, ask the user: "Would you like to improve the base-show skill itself using skill-creator? You can suggest changes to the workflow or quality criteria, or I can analyse what came up during this session to identify improvements to the skill."
Common patterns
Two-overload pattern (always write both together)
Always define the 2-arg and 3-arg MIME overloads as a pair. The MIME method's compact
branch calls the 2-arg method, so both display paths (REPL and in-container) converge
on the same one-liner without repetition:
function Base.show(io::IO, ::MIME"text/plain", x::MyProcess)
if get(io, :compact, false)
show(io, x)
else
println(io, "MyProcess")
# ... full multi-line body ...
end
end
function Base.show(io::IO, x::MyProcess)
print(io, "MyProcess (", nameof(typeof(x.process)), ", N_ens=", x.N_ens, ")")
end
Without the 2-arg method, [ekp] in a Vector falls back to Julia's default field
dump. Without the compact branch in the MIME method, the same dump appears whenever
the object is embedded in a container that happens to call show(io, MIME"text/plain", x) with :compact => true.
Truncate long collections with "… and N more"
When a type holds a variable-length collection, cap the loop to keep output bounded:
function Base.show(io::IO, ::MIME"text/plain", x::ParameterDistribution)
n = length(x.name)
println(io, "ParameterDistribution with ", n, " entr", n == 1 ? "y" : "ies")
max_show = 8
for i in 1:min(n, max_show)
println(io, " '", x.name[i], "': ", sprint(summary, x.distribution[i]))
end
n > max_show && println(io, " … and ", n - max_show, " more")
end
Conditional fields
Only print a field when it carries information:
if !isnothing(x.prior_mean)
println(io, " prior_dim: ", length(x.prior_mean))
end
Pluralisation in summary
Match English grammar for counts that can be 0 or 1:
print(io, "Observation (", n, " block", n == 1 ? "" : "s", ", dim=", dim, ")")
Arrow notation for mappings
Use → in summary when the type represents a transformation between spaces:
print(io, "PairedDataContainer (", m_in, "×", n_in, " → ", m_out, "×", n_out, ")")
Unicode in mathematical contexts
Use × for matrix dimensions, → for transformations, ∞ for unbounded constraints,
and |u| for set sizes. These are rendered cleanly in all modern Julia terminals and
communicate mathematical meaning concisely.
# Constraint summary: Constraint{NoConstraint} (−∞, ∞)
lb = get(bounds, "lower_bound", "-∞")
ub = get(bounds, "upper_bound", "∞")
print(io, "Constraint{$(T)} ($(lb), $(ub))")
Use nameof for parametric type identity
When a type carries a type-parameter that identifies its variant, use nameof rather
than printing the full parameterised name:
# Sampler{Float64} (prior_dim=12) — not the raw Sampler{Float64, ...} dump
print(io, "Sampler{", nameof(get_sampler_type(x)), "} (prior_dim=", length(x.prior_mean), ")")
Section separators in show.jl
When collecting all methods in a dedicated show.jl, organise by type family with
aligned comment rulers:
# ── DataContainers ────────────────────────────────────────────────────────────
# ── Observations ─────────────────────────────────────────────────────────────
# ── EnsembleKalmanProcess ────────────────────────────────────────────────────
Quality criteria
| Criterion | Priority | Definition |
|---|
| Coverage | High | Every type classified as noisy in Step 2 has a Base.show (both overloads) and a Base.summary method. |
| Compact support | High | The 3-arg MIME show checks get(io, :compact, false) and calls the 2-arg show(io, x) in the compact branch. The 2-arg method produces exactly one line with no newline. |
| Brevity — show | High | Full (non-compact) show output is at most 10 lines for any valid instance, including edge cases. |
| Brevity — summary | High | Summary output is exactly one line (no newlines) for any valid instance. |
| Safety | High | Neither method throws on any valid instance. |
| Allocation-safety | High | All data access is O(1): use length(), size(), isempty(), or first() on lazy iterators. Never call collect(), sort(), filter(), or any function that materialises a new collection. |
| Test robustness | Medium | Tests assert structural properties, not exact strings. Cosmetic changes do not break tests. |
| No regression | High | Pre-existing tests continue to pass; no unintended changes to other source files. |
Formatting rules
- MIME show signature:
Base.show(io::IO, ::MIME"text/plain", x::MyType)
- MIME show structure: always starts with
if get(io, :compact, false); show(io, x); else ... end.
- MIME show full branch — first line: type name via
println(io, "TypeName"). Cheap size hints may follow on the same line.
- MIME show full branch — subsequent lines: indented two spaces for readability.
- 2-arg show signature:
Base.show(io::IO, x::MyType)
- 2-arg show content: one
print call (no println), type name followed by a parenthesised hint matching Base.summary style, e.g. print(io, "MyType (847 basins)").
- summary signature:
Base.summary(io::IO, x::MyType)
- summary content: one
print call (no println), type name followed by a parenthesised hint, e.g. print(io, "MyType (847 basins)").
- No collection contents: print only counts, sizes, or ranges — never iterate and print elements.
- No allocations: use
length(), size(), isempty(), and first() on lazy iterators such as values(dict). Do not call collect(), sort(), or any function that copies a collection.
- Tests — MIME full show: use
sprint(show, MIME("text/plain"), x) to capture output without side effects.
- Tests — compact show: use
sprint(show, MIME("text/plain"), x; context=:compact => true) to exercise the compact branch, and sprint(show, x) to test the 2-arg method directly.
- Tests — summary: use
sprint(summary, x) to capture the one-line description.
Examples
Example 1 — matrix-carrying type (size hint)
# Scenario: a type wraps a parameter matrix and a forward-model output matrix.
# Before (default Julia show — prints the full matrix)
julia> pdc
PairedDataContainer{Float64}(inputs=DataContainer{Float64}(data=[...50×100 matrix...]),
outputs=DataContainer{Float64}(data=[...30×100 matrix...]))
# After — custom show (two overloads)
function Base.show(io::IO, ::MIME"text/plain", x::PairedDataContainer)
if get(io, :compact, false)
show(io, x)
else
m_in, n_in = size(x.inputs.data)
m_out, n_out = size(x.outputs.data)
println(io, "PairedDataContainer")
println(io, " inputs : ", m_in, " × ", n_in, " params × samples")
println(io, " outputs: ", m_out, " × ", n_out, " obs × samples")
end
end
function Base.show(io::IO, x::PairedDataContainer)
m_in, n_in = size(x.inputs.data)
m_out, n_out = size(x.outputs.data)
print(io, "PairedDataContainer (", m_in, "×", n_in, " → ", m_out, "×", n_out, ")")
end
# julia> pdc
# PairedDataContainer
# inputs : 50 × 100 params × samples
# outputs: 30 × 100 obs × samples
# julia> [pdc, pdc]
# 2-element Vector{PairedDataContainer{Float64}}:
# PairedDataContainer (50×100 → 30×100)
# PairedDataContainer (50×100 → 30×100)
# After — custom summary (arrow notation for a mapping type; matches 2-arg show)
function Base.summary(io::IO, x::PairedDataContainer)
m_in, n_in = size(x.inputs.data)
m_out, n_out = size(x.outputs.data)
print(io, "PairedDataContainer (", m_in, "×", n_in, " → ", m_out, "×", n_out, ")")
end
Example 2 — collection-carrying type with truncation
# Scenario: a type holds N named parameter distributions; N can be large.
# Before (default Julia show — prints every distribution in full)
julia> prior
ParameterDistribution{Parameterized, Constraint{NoConstraint}, String}(
distribution=[Parameterized(Normal{Float64}(μ=0.0, σ=1.0)), ...],
constraint=[[Constraint{NoConstraint}(bounds=nothing)], ...],
name=["amplitude", "length_scale", "noise_var", ...])
# After — custom show (two overloads)
function Base.show(io::IO, ::MIME"text/plain", x::ParameterDistribution)
if get(io, :compact, false)
show(io, x)
else
n = length(x.name)
println(io, "ParameterDistribution with ", n, " entr", n == 1 ? "y" : "ies")
max_show = 8
for i in 1:min(n, max_show)
n_con = length(batch(x)[i])
println(io, " '", x.name[i], "': ", sprint(summary, x.distribution[i]),
" [", n_con, " constraint", n_con == 1 ? "" : "s", "]")
end
n > max_show && println(io, " … and ", n - max_show, " more")
end
end
function Base.show(io::IO, x::ParameterDistribution)
n = length(x.name)
print(io, "ParameterDistribution (", n, " entr", n == 1 ? "y" : "ies", ")")
end
# julia> prior
# ParameterDistribution with 3 entries
# 'amplitude' : Parameterized (Normal) [1 constraint]
# 'length_scale': Parameterized (LogNormal) [1 constraint]
# 'noise_var' : Parameterized (Uniform) [1 constraint]
# julia> [prior, prior]
# 2-element Vector{ParameterDistribution{...}}:
# ParameterDistribution (3 entries)
# ParameterDistribution (3 entries)
# After — summary (matches 2-arg show)
function Base.summary(io::IO, x::ParameterDistribution)
n = length(x.name)
print(io, "ParameterDistribution (", n, " entr", n == 1 ? "y" : "ies", ")")
end
Example 3 — stateful iterative process
# Scenario: a mutable struct accumulates state across EKI iterations.
# Before (default Julia show — dumps every matrix stored in the struct)
julia> ekp
EnsembleKalmanProcess{Float64, ...}(u=[50×100 matrix, 50×100 matrix, ...],
g=[...], Δt=[0.5, 0.5], rng=MersenneTwister(...), N_ens=100, ...)
# After — custom show (two overloads)
function Base.show(io::IO, ::MIME"text/plain", x::EnsembleKalmanProcess)
if get(io, :compact, false)
show(io, x)
else
n_iter = length(x.u) - 1
n_par = size(x.u[1].data, 1)
println(io, "EnsembleKalmanProcess")
println(io, " process : ", nameof(typeof(x.process)))
println(io, " N_ens : ", x.N_ens)
println(io, " N_par : ", n_par)
println(io, " n_iter : ", n_iter)
println(io, " scheduler : ", nameof(typeof(x.scheduler)))
println(io, " accelerator: ", nameof(typeof(x.accelerator)))
end
end
function Base.show(io::IO, x::EnsembleKalmanProcess)
n_iter = length(x.u) - 1
print(io, "EnsembleKalmanProcess (", nameof(typeof(x.process)),
", N_ens=", x.N_ens, ", ", n_iter, " iter)")
end
# julia> ekp
# EnsembleKalmanProcess
# process : Inversion
# N_ens : 100
# N_par : 50
# n_iter : 5
# scheduler : DefaultScheduler
# accelerator: DefaultAccelerator
# julia> [ekp, ekp]
# 2-element Vector{EnsembleKalmanProcess{...}}:
# EnsembleKalmanProcess (Inversion, N_ens=100, 5 iter)
# EnsembleKalmanProcess (Inversion, N_ens=100, 5 iter)
# After — summary (matches 2-arg show)
function Base.summary(io::IO, x::EnsembleKalmanProcess)
n_iter = length(x.u) - 1
print(io, "EnsembleKalmanProcess (", nameof(typeof(x.process)),
", N_ens=", x.N_ens, ", ", n_iter, " iter)")
end
Unit tests
@testset "PairedDataContainer show" begin
pdc = PairedDataContainer(rand(50, 100), rand(30, 100))
out = sprint(show, MIME("text/plain"), pdc)
@test occursin("PairedDataContainer", out)
@test count(==('\n'), out) <= 10
end
@testset "PairedDataContainer show compact" begin
pdc = PairedDataContainer(rand(50, 100), rand(30, 100))
# exercise via the 2-arg method directly
out2 = sprint(show, pdc)
@test occursin("PairedDataContainer", out2)
@test !occursin('\n', out2)
# exercise via the MIME method with compact context
out3 = sprint(show, MIME("text/plain"), pdc; context=:compact => true)
@test out2 == out3 # both paths must agree
end
@testset "PairedDataContainer summary" begin
pdc = PairedDataContainer(rand(50, 100), rand(30, 100))
out = sprint(summary, pdc)
@test occursin("PairedDataContainer", out)
@test !occursin('\n', out) # must be exactly one line
end