Displaying Rationals

I have a small complaint about julia. I use all the time matrices of Rationals, like

> 10×10 Array{Rational{Int64},2}:
>  2//1  0//1   0//1   0//1  0//1   0//1   0//1   0//1   0//1   0//1
>  0//1  2//1   0//1   0//1  0//1   0//1   0//1   0//1   0//1   0//1
>  0//1  0//1   4//3  -2//3  2//3   0//1   0//1   2//3  -2//3  -2//3
>  0//1  0//1  -2//3   4//3  2//3   0//1   0//1   2//3  -2//3  -2//3
>  0//1  0//1   2//3   2//3  1//3   1//1   1//1   1//3   2//3   2//3
>  0//1  0//1   0//1   0//1  1//1   1//1  -1//1  -1//1   0//1   0//1
>  0//1  0//1   0//1   0//1  1//1  -1//1   1//1  -1//1   0//1   0//1
>  0//1  0//1   2//3   2//3  1//3  -1//1  -1//1   1//3   2//3   2//3
>  0//1  0//1  -2//3  -2//3  2//3   0//1   0//1   2//3   4//3  -2//3
>  0//1  0//1  -2//3  -2//3  2//3   0//1   0//1   2//3  -2//3   4//3

I find it perfectly acceptable that to input a Rational, you have
to write a//b. But I find pretty horrible that they are output the same
way. I would like to see:

10×10 Array{Rational{Int64},2}:
2 0    0    0   0  0  0   0    0    0
0 2    0    0   0  0  0   0    0    0
0 0  4/3 -2/3 2/3  0  0 2/3 -2/3 -2/3
0 0 -2/3  4/3 2/3  0  0 2/3 -2/3 -2/3
0 0  2/3  2/3 1/3  1  1 1/3  2/3  2/3
0 0    0    0   1  1 -1  -1    0    0
0 0    0    0   1 -1  1  -1    0    0
0 0  2/3  2/3 1/3 -1 -1 1/3  2/3  2/3
0 0 -2/3 -2/3 2/3  0  0 2/3  4/3 -2/3
0 0 -2/3 -2/3 2/3  0  0 2/3 -2/3  4/3

As it is, I have the feeling that I must change my glasses whenever I print
a matrix of Rationals. As I understood, you would just have to change (or to
write a new)

function Base.display(x::Rational)

to do the trick. Do people think this would be reasonable?

1 Like

It would be weird to not be able to copy them and have it keep the same type.

6 Likes

I thought the function show and not the function display was the one where you should be able to parse back the output. Anyway you cannot copy as is the output: you would have to suppress the summary and add . I certainly would not change the function show so you could still copy its output and get the same type.

No, you should not overload display. Not only would this not affect output as part of a matrix, but the manual explicitly tells you not to overload display. To customize output, you use show.

One option would be to define a show method for Array{<:Rational}.

A more permanent solution would be to modify the Base display of Rational types. In particular, when a rational value is shown in an array, the array sets the :compact IO context attribute to true. It might be reasonable to show rationals as p/q for :compact=>true output. In particular, you could submit a JuliaLang/julia PR that modifies the show(io, ::Rational) method to something like:

function show(io::IO, x::Rational)
    show(io, numerator(x))
    print(io, get(io, :compact, false) ? "/" : "//")
    show(io, denominator(x))
end

With this change, arrays of rationals will use a single slash, but showing a single rational value in the REPL will still use a double slash. The only problem is that it would affect repr of arrays too, meaning the repr output would not be parseable to reproduce the input.

Another option would to have a different IO context attribute to suppress the double slash in rational values, which could be set by e.g. show(io::IO, ::MIME"text/plain", X::AbstractArray). Maybe there should be a more general IO context attribute for whether the output needs to be parseable?

I encourage you to try submitting a PR for this, which should be relatively easy (just the code patch above and a test). I can’t guarantee that it will be accepted, but I think there is a reasonable chance that some variant of this change could go in, and it hopefully would be a good experience for you in any case.

5 Likes

But when a rational object is converted to a string, I hope it still retains the // because, for example in my packages it is needed to parse rational julia objects into rational values in another programming language, and the behavior of / and // is supposed to be different when parsing the text of Julia code.

That’s why we might want an IO context attribute to indicate whether the show output should be parseable etc.

I would like to only modify the display of Rational arrays in the REPL, nothing else since indeed you
want that all functions usually used for output like show(x) and print(x) give parseable output. So I guess
this needs to follow your suggestion to use an IOContext set by

show(io::IO, ::MIME"text/plain", X::AbstractArray)

To have an uncluttered display like I showed above, I would need to modify slightly your code to

   show(io, numerator(x))
   if  get(io, :compact,true) 
        if denominator(x!=1)
           print(io,"/")
           show(io,denominator(x))
        end
   else
       print(io, "//")
       show(io, denominator(x))
   end

Do you think that :compact is the right io property or another one should be used?

Here’s an implementation that uses the lighter format in the REPL, without affecting show(A) and repr(A):

import Base.show

function show(io::IO, x::Rational)
    show(io, numerator(x))
    # The default is to not use light format
    if get(io, :lightrationals, false)
        if denominator(x) != 1
            print(io, "/")
            show(io, denominator(x))
        end
    else
        print(io, "//")
        show(io, denominator(x))
    end
end

function show(io::IO, mime::MIME"text/plain", X::AbstractArray{<:Rational})
    # Set light rational format if currently undefined
    if !haskey(io, :lightrationals)
        io = IOContext(io, :lightrationals => true)
    end
    invoke(show, Tuple{IO, MIME"text/plain", AbstractArray}, io, mime, X)
end

And in Julia 1.4 it can be then disabled easily :slight_smile: Just do:

Base.active_repl.options.iocontext[:lightrationals]=false

I didn’t change how scalars are displayed, because that would be confusing I think: nothing would indicate that 2 is actually 2//1 for example. This is not a problem with arrays (even if all rationals happen to be integers) since the type is shown in the summary at the top. But in case someone wants it for scalars too:

function show(io::IO, ::MIME"text/plain", x::Rational)
    if !haskey(io, :lightrationals)
        io = IOContext(io, :lightrationals => true)
    end
    show(io, x)
end

@stevengj do you still think this would make a good PR? If yes, @Jean_Michel would you like to make the PR?

1 Like

You can make the PR. I learned some git only very recently and don’t know yet how to make one. Perhaps your key :lightrationals could be :usualrationals or :standardrationals or any other word which suggests that it is the way they are represented in mathematics or in most programming languagues.

OK I’ll probably make a PR this weekend and put the link here, but feel free to do it first if you want to exercise git skills :slight_smile:

I would not spend time preparing a PR, since I don’t agree that this is a good feature.

For the same reason that 1.0 is not displayed as 1, we should not add confusion by printing 1//2 as 1/2. It is horrible if input and output differ for basic number types.

5 Likes

Let me try to convince you :slight_smile:

I think it’s bad user experience to show arrays with the 0//1 format. Users have to deal with the hard-to-read output every time they print a rational array, just in case they want to copy-paste the values…

And it’s not like all standard array types print in a way that preserves the type on copy-paste… Example:

julia> rand(Int8, 6, 6)
6×6 Array{Int8,2}:
  -40    55   -2    24   39   69
   71    40  -10   -36   57   44
   48    70    1   -54  -31   93
 -114   118  -46    61   15  -26
   57   124  -44  -124  -99   77
   49   -91  -65    83  -10  101

Copy-pasting this will make an Array{Int64} instead of an Array{Int8}.

Now the big difference is that it’s trivial to fix the Int8 case by pasting inside Int8[...]. So what about this:

Let’s define the operator (“division slash” character in Unicode) as synonym to //. Then if we use it to print rational arrays, they will be trivial to copy-paste to the correct type (in the worst case, when you have all integer values, you need to add a Rational[...] prefix like for Int8).

If is deemed to close to /, we could also use the “big solidus”:

What do you think?

2 Likes

Sorry, but using almost identical looking unicode characters for two opposite operations does not sound like an improvement.

2 Likes

Isn’t 1⧸2 is quite distinct from 1/2? And the / and // operations are hardly opposite?

Before instinctively rejecting, please pay more attention to the exact proposal: give a nicer display of Rationals only in circumstances where the type is also displayed proeminently.

I personally think that instead of this:

5×5 Array{Rational{Int64},2}:
 1//2  0//1  0//1  0//1  0//1
 0//1  1//2  0//1  0//1  0//1
 0//1  0//1  1//2  0//1  0//1
 0//1  0//1  0//1  1//2  0//1
 0//1  0//1  0//1  0//1  1//2

This is easier to read:

5×5 Array{Rational{Int64},2}:
 1/2    0    0    0    0
   0  1/2    0    0    0
   0    0  1/2    0    0
   0    0    0  1/2    0
   0    0    0    0  1/2
3 Likes

FWIW, we do essentially this same thing for boolean arrays in 1.2+. I think such a thing could also be reasonable for rationals (but I almost never use them so I don’t have a horse in this race). The implementation is very simple — it’s just a matter of asking the IO stream if it has displayed type information:

https://github.com/JuliaLang/julia/pull/30575/files#diff-b4267643a238b5a375b3e055817ee6caR567

4 Likes

As I stated earlier, for parsing purposes it will mess things up, since parsing a rational number with a single slash will become a float when evaluated. This will make things confusing because when you write code with something like 1/2 it becomes a float if you are parsing, while it might be the display of a rational. This means that writing a number in code and parsing it from an output will give different results.

If you really want to change it, then the default has to be changed so that 1/2 is rational by default when writing code also. If you’re gonna change it, the entire syntax should be switched over, going only half way and doing it only for the display will lead to confusion.

Since this is a breaking change, it has to wait until 2.0

It would break code which relies on parsing rationals.

Just to be clear, tweaking the printed output in the REPL for some Base type is not considered a breaking change (and tends to happen in every minor release)…

2 Likes

In my post I am talking about also changing the behavior of / function to make rationals, so that is a breaking change, since I recommend going all the way and changing the / function instead of going half way to only change the display. That would be breaking.

Either go all the way with it or dont change it.

You seem to be missing the part about this only having an effect when printing lots of rationals inside a well-typed container — just like how we don’t display true and false as 1 and 0 generally. This is a limited case with lots of repeats; and the superfluous information can certainly get in the way.

At a minimum, it seems like it could be worthwhile to avoid //1 for whole numbers.

3 Likes