Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

set_display_precision!(n) in REPL to control floating-point display #52543

Open
wants to merge 8 commits into
base: master
Choose a base branch
from

Conversation

stevengj
Copy link
Member

@stevengj stevengj commented Dec 15, 2023

Closes #6493: gives a way to control the REPL display precision (the number of significant digits displayed for floating-point values, and more generally the printf-style format string).

This is a very common request from users. e.g. on discourse see here and here and here and here and here. A lot of the suggested solutions involve redefining show(::IO, ::Float64), which is type piracy and non-composable.

I wanted a solution which:

  • Is composable: only affects display output (what the REPL uses to show results) and not output to any other stream (files, etcetera).
  • Does not affect performance for any non-display I/O.
  • Can be configured independent of the REPL (e.g. the same configuration can be used for IJulia etc), and works in both startup.jl and a running REPL (or IJulia etc) session. But if you somehow have multiple REPLs running in parallel from the same Julia process, they can still change their display formats independently.
  • Is extensible to floating-point types in other packages (opt-in).

This PR adds two new functions exported by the InteractiveUtils stdlib (which is loaded in REPL and IJulia sessions):

set_display_precision!([display], [T=AbstractFloat], precision; compact=false)
unset_display_precision!([display], [T=AbstractFloat]; compact=false)

to set the displayed precision to precision, which can either be an integer number of significant digits (corresponding to a format string "%.*g") or an arbitrary printf format string, for the type T and its subtypes (defaulting to AbstractFloat, i.e. all supported floating-point types). If you don't pass a display argument, it affects all currently loaded AbstractDisplays (that support this settting) as well as a dictionary of defaults that is used for subsequently created REPL sessions (or IJulia sessions, in the future). By default, it affects the digits shown for all displays, but you can separately control the precision shown in :compact=>true IOContexts by passing the compact=true argument.

Internally, a supporting display (currently just REPLDisplay, but IJulia can opt-in to this later) implements this by wrapping its IO stream in an InteractiveUtils.digitsio(io) stream, which overloads show for the built-in AbstractFloat types (and external packages can opt-in for their own AbstractFloat types in the future). This way it has zero effect on any other IO stream. (IOContext wrappers around a DigitsIO stream inherit its floating-point display too.)

For example:

julia> z = rand(ComplexF64)
0.7230118898045041 + 0.33728384864961525im

julia> set_display_precision!(8)

julia> z                # now displays with 8 digits
0.72301189 + 0.33728385im

julia> A = rand(2,2)    # compact display now uses 8 digits too
2×2 Matrix{Float64}:
 0.34100692  0.21678433
 0.23189211  0.68572311

julia> set_display_precision!(2, compact=true)

julia> A                # compact display now uses 2 digits
2×2 Matrix{Float64}:
 0.34  0.22
 0.23  0.69

julia> z                # non-compact display still uses 8 digits
0.72301189 + 0.33728385im

As mentioned above, you can also use an arbitrary printf format string:

julia> set_display_precision!("my floats show 32 significant digits: %0.32g")

julia> pi * 1e10
my floats show 32 significant digits: 31415926535.897930145263671875

julia> set_display_precision!("my floats show 32 digits after the decimal point: %0.32f")

julia> pi * 1e10
my floats show 32 digits after the decimal point: 31415926535.89793014526367187500000000000000

Remaining issues

Before I do any more work on this, however, I want to get some feedback on the overall design. Do the core devs want something along these lines?

To do:

  • Bikeshedding the names and other minor details
  • Documentation
  • Tests

There is also the question of whether we only want to control digits (always format as %.*g) or if we want a configurable format string (con: more complex; pro: more options, but is it actually useful in practice?). Configurable format strings could be added later on if desired — you'd still want a simple API where you just specify the precision, as well as a more advanced API where you pass a format string — but it would be easier for downstream packages (e.g. IJulia) if we build it in now. IPython's %precision allows either an integer or a format string, so that may be a reasonable precedent for allowing a similar flexibility in Julia. (Maybe rename to set_display_precision!) Update: now optionally supports arbitrary format strings instead of a numeric precision, though I expect that most users will opt for the latter.

@stevengj stevengj added speculative Whether the change will be implemented is speculative REPL Julia's REPL (Read Eval Print Loop) needs tests Unit tests are required for this change needs docs Documentation for this change is required needs news A NEWS entry is required for this change display and printing Aesthetics and correctness of printed representations of objects. labels Dec 15, 2023
@stevengj stevengj changed the title add set_display_digits(n) to REPL to control floating-point display add set_display_digits!(n) in REPL to control floating-point display Dec 15, 2023
@stevengj stevengj changed the title add set_display_digits!(n) in REPL to control floating-point display set_display_digits!(n) in REPL to control floating-point display Dec 15, 2023
@stevengj stevengj changed the title set_display_digits!(n) in REPL to control floating-point display set_display_precision!(n) in REPL to control floating-point display Dec 18, 2023
@stevengj
Copy link
Member Author

@StefanKarpinski, should I continue to pursue this?


PrecisionIO(io::IO, precision::AbstractDict, compact_precision::AbstractDict) =
new{typeof(io)}(io, precision, compact_precision)
end
Copy link
Member Author

@stevengj stevengj Dec 19, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The alternative to defining a new PrecisionIO <: IO subtype to propagate this information is to simply use IOContext with a :precision => <digits> option. Using a plain IOContext has both advantages and disadvantages:

Advantages of using IOContext:

  • Simpler, more user-accessible implementation. (Documented with the other IOContext flags.)
  • Easier for other packages (e.g. DecFP.jl) to support for their own floating-point types (don't have to depend on InteractiveUtils.PrecisionIO internals).
  • More consistent with our other IO metadata (:compact etc)
  • Easier to use in non-display contexts, e.g. if the user wants to write to a CSV file with fewer digits.

Disadvantages of using IOContext:

  • Can only support %.*g format, not arbitrary user-defined format strings. (Since otherwise base/ryu/Ryu.jl would have to depend on Printf.)
  • Less fine-grained control (probably we would just want precision to be an integer, apply to all supporting floating-point types, and override :compact unconditionally).
  • IOContext flags are a global namespace, hence precious and poorly composable — it's not clear if any package is already using :precision for something else?
  • Requires some refactoring of Printf stdlib, to move %.*g formatting to Ryu.jl.

Overall, I'm starting to be inclined more towards the IOContext route. It's not clear to me that fine-grained control here (different precisions for different types, or compact vs non-compact mode) is particularly important — people mainly just want a way to show more (or fewer) digits in the REPL.

What do people think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IOContext does seem like a more natural way to implement this.

@StefanKarpinski
Copy link
Member

This seems good! I had not seen it, but I will look now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
display and printing Aesthetics and correctness of printed representations of objects. needs docs Documentation for this change is required needs news A NEWS entry is required for this change needs tests Unit tests are required for this change REPL Julia's REPL (Read Eval Print Loop) speculative Whether the change will be implemented is speculative
Projects
None yet
Development

Successfully merging this pull request may close these issues.

function to change numeric output format
3 participants