Proper rational numbers (prototype for testing)

Following discussion started by @ridgeworks and followed up on github, I’ve pushed a branch rational that realises atomic rational numbers. It compiles and passes the tests. There are still several issues:

  • Finish the discussion on the final syntax. Currently simply uses e.g. 1/3. This smells fishy, but seems cause very few or no problems in practice.
  • Sync with ECLiPSe if possible (notably syntax is hard as ECLiPSe uses 1_3, which conflicts with our integer digit grouping.
  • Testing and unit tests. Volunteers?
  • Test on real applications. Does anything break or get seriously slower?
  • Rational number arithmetic can become crazy big. Should we install a tripwire system to warn/error/go to float mechanism to detect such cases?

If you want to try and have an active checked out version of the git sources, simply do

git fetch
git checkout rational
ninja

Switch back to branch master in a similar way.

3 Likes

Compiling the rational branch results in three warnings that don’t occur with the master branch:

[271/2362] Building C object src/CMakeFiles/libswipl.dir/pl-wic.c.o
../src/pl-wic.c:1529:29: warning: absolute value function 'abs' given an argument of type 'ssize_t' (aka 'long') but has parameter of type 'int' which may cause truncation of value [-Wabsolute-value]
                mpz_load_bits(fd, p, mpz, abs(hdrsize));
                                          ^
../src/pl-wic.c:1529:29: note: use function 'labs' instead
                mpz_load_bits(fd, p, mpz, abs(hdrsize));
                                          ^~~
                                          labs
../src/pl-wic.c:1554:29: warning: absolute value function 'abs' given an argument of type 'ssize_t' (aka 'long') but has parameter of type 'int' which may cause truncation of value [-Wabsolute-value]
                mpz_load_bits(fd, p, num, abs(num_hdrsize));
                                          ^
../src/pl-wic.c:1554:29: note: use function 'labs' instead
                mpz_load_bits(fd, p, num, abs(num_hdrsize));
                                          ^~~
                                          labs
../src/pl-wic.c:1557:29: warning: absolute value function 'abs' given an argument of type 'ssize_t' (aka 'long') but has parameter of type 'int' which may cause truncation of value [-Wabsolute-value]
                mpz_load_bits(fd, p, den, abs(den_hdrsize));
                                          ^
../src/pl-wic.c:1557:29: note: use function 'labs' instead
                mpz_load_bits(fd, p, den, abs(den_hdrsize));
                                          ^~~
                                          labs
3 warnings generated.

Logtalk fails to start with this branch. Debugging suggest a failure when returning from a predicate call (that succeeds) to the next goal in a conjunction. Something seems broken at a low level.

So true. For my application I convert a rational to a float whenever:
abs(Numerator)+(Denominator) > max_tagged_integer

but this is a fairly conservative limit and may, in general, not be the best. It’s also important that the rounding direction be specifiable when precision is lost since I’m using a pair of numeric values as closed bounds.

Not sure what a general solution would look like, but this may be useful as a starting point.

I’m happy to do some testing if you can point me at an executable I can run on my Mac (terminal mode acceptable).

Can it be combined with 0b 0x prefix? In LISP something similar is possible:

2.3.2.1.2 Syntax of a Ratio
http://www.lispworks.com/documentation/lw50/CLHS/Body/02_cbab.htm

Does 1 / 2, with spaces before and after the slash, still evaluate to 0.5?

Thanks for the quick test! Pushed a fix for the MacOS warnings, though they will not break anything.

Looked at the Logtalk starting issue. This is a bit hard as you disabled most tracing :slight_smile: Anyway, with some trace/1 calls I got some idea what went wrong after verifying that work fine with

?- set_prolog_flag(rational, false).

The trouble is with the Logtalk info statement

date is 2016/07/12,

Which is then handled in '$lgt_compile_entity_info_directive_pair' using

Date = Year/Month/Day

Finally your throw(type_error(date, Date)) apparently is caught before reaching the user, so this was a bit hard to find :frowning:

Now the question is what to learn from this? 2016/07/12 is parsed to a term /(Rat,12) where rat is the rational number following from 2016/07, which is turned into the canonical representation 288. This means that effectively this doesn’t work and there is no (meaningful) way to get 2016/07 from this.

There seem to be a couple of ways out

  • Forget about the idea to use 3/1 as a rational and go back to one of the more exotic ways to write rational numbers.
  • Do not perform canonicalization of rationals in read/1, so we can trap the rational and get the original integers to deal with notations like this. Note that this is as loss-free as the original. You can the integers, but 7 rather than 07. This basically allows readying almost any document containing rationals as before, but you need to do some work to get back the original terms. Documents containing 2016/00 remain illegal.
  • Consider that using Y/M/D for a date term is a dubious choice and insist on atoms/strings. Note that there are many date notations

Note that the problem that made this hard to debug was three fold

  • Disabling debugging the Logtalk core
  • Logtalk loading your program by term0reading rather than compiling. After loading normal Prolog code that contains unwanted rationals, list_rationals/0 lists possible problems.
  • The Logtalk internal error getting lost somewhere.
1 Like

The tripwire value should of course be configurable. Automatic translation to a float is rather dubious. Basically code that better used floats should be fixed by turning some constant in the computation from integer to float (in most cases) or using float/1. A tripwire can be handy to make the programmer aware and automatic translation to float could be a way make the transformation smoother.

Yes. The numerator and denominator can use any syntax valid for Prolog integer. The 0x prefix of the numerator currently does not imply a 0x for the denominator though. Seems Lisp does this. Not sure this is a good or bad idea. What about other languages?

Currently not. As (violating ISO), - 42 is a compound term in SWI-Prolog.

There are no daily executable for the Mac. It isn’t too hard to build from source though.

That rational flag is handy :slightly_smiling_face: Sorry that I missed it.

2016/07/12 is a perfectly valid Prolog term and a natural representation for dates. An implementation of rational numbers should not change its meaning. Nothing Logtalk specific here.

Debugging only appears disabled in some Prolog systems. The Logtalk compiler/runtime is written in portable Prolog code (with a few bits written in Logtalk itself such as the built-in entities). Other Prolog systems don’t have any issue with debugging the code and I use their debuggers when the need arises.

I’m not sure what you mean by “rather than compiling”. True, the Logtalk compiler uses read_term/3 to compile source files. That’s the only viable solution to cope with all the syntax differences and quirks found in every Logtalk supported Prolog system. The Logtalk compiler necessarily type checks Logtalk source code before generating intermediate Prolog code. Thus, the “unwanted rationals” already caused a type-check error before the Prolog compiler sees the intermediate code. But that’s not what hidden the issue here, as explained next.

The internal error is not lost. If a invalid date is used in the info/1-2 directives, you get:

?- {loader}.
!     Type error: expected integer but got a
!       in directive info/1
!       in file /Users/pmoura/Documents/Logtalk/logtalk3/examples/ack/ack.lgt between lines 23-28

But, in this case, the errors happens as soon as the first built-in entity is compiled and that is done silently as compilation errors in the built-ins entities are only an issue when a buggy or a beta build of a backend Prolog system is used (as is the case here). In particular, the code that handles printing of compiler errors and warnings was never loaded as it’s found in one of the built-in entities that is not yet loaded when the error occurs.

The documentation isn’t very accessible yet (although apropos/1 and help/1 give the up-to-date docs). This flag may change to prefer_rationals as this is its ECLiPSe name. Only, in ECLiPSe is doesn’t affect the syntax as well because their 1_3 syntax is otherwise not valid Prolog. SWI-Prolog uses the underscore to allow for 1_000_000, which is also highly appreciated by some users. So, we have a problem :frowning:

I haven’t really sorted out why. We had several discussions on this. Normally '$' predicates are locked as non-debug predicates, but using ?- set_prolog_flag(access_level, system). re-enables debugging (with some tweaks that it starts debugging SWI-Prolog internals, so the experience is a little less human friendly). In fact this could all be relaxed. SWI-Prolog only uses '$' predicates in init.pl that is loaded into the module system. Anyway, that is a side topic.

So, yes, 2016/07/12 is normally a totally valid Prolog term. The issue is whether we want to sacrifice this, allowing for 1/3 as most languages that support rationals use or do we want an awkward syntax for rationals? According to @ridgeworks we have the following prior art (last line by me).

P/Q : Common Lisp, Python (fractions), OWL, Guile, Wolfram
P//Q : Julia (also allows Q to be signed)
P/Qr : Ruby
P_Q : ECLiPSe

None of these work in SWI-Prolog. The P/Qr solution could be made to work as P/QR (e.g, 1/3R), which is invalid Prolog syntax, but notable Joachim objects, claiming it is too much stress on the tokenizer to demand that much look-ahead. I don’t like it for being ugly as well as hard to handle but could live with it if it was widely used. But, it is only Ruby and then we also need to make a variation. Joachim also suggested P_/Q, which indeed breaks nothing, but hurts my eyes a bit :(.

So, I started to play with the idea to be brave and simply turn 1/3 into a rational. I’ve ran a lot of my Prolog programs through it and found only issues in some unit tests (e.g, using A is 1/0 to test error handling but now 1/0 is an illegal number an load time rather than an evaluation error at run time). You found a more serious counter example. Is this a show-stopper? Or can/should we provide a work-around? Possible work-around are

  • Not make the rational canonical, so the application can turn them loss-free back to a term in places where a term is expected.
  • Provide an option in read_term/3, though that would basically turn rational syntax off for Logtalk.

IMO changing the reading interpretation of 1/3 from being a term is an huge breakage…

Consider also the difference between
A = 1, B = 3, C is A/B.
and
C is 1/3.

If indeed a non-term syntax is needed for rationals I suggest we use something that currently is not a valid syntax.

3 Likes

For completeness: missing from my original list (see Rational_data_type):

PrQ : J

Also add Smalltalk to the P/Q list.

Given the full proposal (Issue #539 on GitHub) there would be no difference. It’s just that C would be a rational number (but a number as opposed to compound) rather than a float. I’m afraid that’s the price to be paid to make rationals first class citizens. And regardless of literal syntax, division of two integers would yield a rational number (or an integer, which is a rational number).

The whole point of Jan’s exercise is to smoke out where this causes real problems, i.e., if a number was actually in rational format internally would it really break anything? He’s grepped a ton of Prolog code and hasn’t come up with anything that would be problematical, as far as I know.

One problem (in Logtalk) that has since come up is the convenience of using / as a date, e.g., 01/01/2020. This happens to work fine in Prolog and is an relevant counter-argument. (I don’t think you’d be able to do this in any(?) other programming language, but they’re usually based on functions which evaluate everything.)

Given a clean slate I think 1/3 would be the right choice for a rational literal, but we don’t have that luxury. Issue #539 has a long discussion on some of the tradeoffs.

I can’t answer the showstopper question, but the workarounds feel a bit contrived (hard to justify). As much as I prefer the P/Q syntax, and in the absence of a better workaround,
it would be better IMO to change the literal syntax.

Not exactly sure how this would work - maybe call a user definable hook which permits application control over when and how to convert a rational to a float?

I wouldn’t mind one of these for dealing with IEEE infinities and NaN’s as well so I wouldn’t have to put all my is's in catch's.

I can confirm that the warnings are gone. Thanks for the quick fix!

Again, this is not a Logtalk issue. Reusing standard operators, including arithmetic operators, is a common practice to avoid the pitfalls of defining new operators. This is a perfectly valid practice as the terms that reuse the operators are used in contexts that avoid mistaken them for the original operator meaning/semantics. The canonical example is representing pairs using the (-)/2 arithmetic operator to be able to write Key-Value. Converting (or attempting to convert) those terms to something else at term parsing time breaks that practice. It’s fine to interpret A/B as a rational term when used as a sub-term in a is/2 goal or in an arithmetic comparison goal. Outside an arithmetic context, it breaks Prolog at a fundamental level.

2 Likes

Totally agree that it’s not a Logtalk issue and sorry if my wording implied that. But I think it’s not quite as black and white as you state. ‘-’ is a prefix operator and ‘.’ and ‘,’ are infix operators, but they aren’t always interpreted as operators, e.g., when used with numbers or as a separator between list elements (or term arguments):

?- current_op(F,P,-), X = -2, atomic(X).
F = 200,
P = fy,
X = -2.

?- current_op(F,P,.), X = 2.0, atomic(X).
F = 100,
P = yfx,
X = 2.0.

?- current_op(F,P,,), ([X] = [a,b] ; true).
F = 1000,
P = xfy.

It’s always been this way, so nobody thinks about it. But this illustrates that it’s not a universally applied principle in Prolog syntax that operators are always operators. IMO using / for rational literals is no different, but there is a potential for conflict with prior uses, especially since / is an attractive character to use in different contexts, e.g., for dates.

This is evaluation (X is 1/2) which is not the issue under discussion. (is and cousins will still see 1/N as a compound term.) The question is given X = 1/2, what type is X? The objective is to find a natural syntax such X is a number; not a compound.

2 Likes

I tend to draw some conclusions from this.

  • The single rational flag to both affect arithmetic and syntax is not a good idea. I’ll move the arithmetic to prefer_rationals to be fully compatible to ECLiPSe.
  • Syntax will remain a hard debate. With @ridgeworks, I agree that handling 1/3 as rational is no different from -2. I’m inclined to add a flag (rational_syntax?) with values
    • natural
      Uses 1/3
    • none
      Adds no syntax extensions for rational numbers. See below.
    • ugly (to be renamed)
      Does something that is invalid syntax in Prolog. Need to think about that, but 1R3 is a good candidate.
    • eclipse (possibly)
      Use 1_3 and stop making this work for digit grouping.

Using none may seem a weird choice, but might also not be too bad. With prefer_rationals to true, this still does

?-  A is 1/2+1/5, atomic(A).
A = 7/10

But, the price is that e.g. 1/2 is 2/4 fails. Using terms 1/2, etc is fine if they end up being used in arithmetic, but not when they are used in rules/facts where they are matched against rationals.

This of course is just an ugly compromise :frowning: Would it be acceptable? Then we have the default, where I’m tempted to go for natural, at least to give it a try. Logtalk should probably change this to something else …

Remains another open question. ECLiPSe considers rationals and integers disjoint and 3_1 is thus different from 3. SWI-Prolog rational support always used integers as canonical rational numbers. I think this is both more efficient and natural. You would get

?- A is 4/2.
A = 2_1.

Joachim already states

Shortcomings
The strict type separation of integers and rationals may not be desirable. An alternative would be to automatically convert integral rationals (i.e. rationals with a normalized denominator of 1) to integers. SWI-Prolog does it this way already. Integers then become a subset of the rationals rather than a disjoint data type.

How do people think about that?

I assume you mean to write “that characters are not always operators”? There are other cases of characters (or sequences of characters) declared as operators that have different meanings depending on context. But to reuse one of your examples, there’s no parsing ambiguity in -2. If we want a compound term instead of a negative integer, we need to write either - 2 (note the space) or '-'(2). The ISO standard makes this messy (compound(- 2) fails and integer(- 2) succeeds in e.g. GNU Prolog and SICStus Prolog; sigh).

The current rational branch creates a conflict by changing the interpretation of a A/B term in all contexts (assuming the rational flag is true) from a compound term to an atomic term. That’s why I claim this (independent of context) reinterpretation breaks Prolog (operator) semantics (as ISO does in - 2, btw).

But, as Jan mentioned in his reply today, ECLiPSe shows that rational numbers support doesn’t necessarily cause conflicts that stop A/B being a compound term. For example:

[eclipse 44]: get_flag(prefer_rationals, Value).

Value = off
Yes (0.00s cpu)
[eclipse 45]: X = 2020/01/01.

X = 2020 / 1 / 1
Yes (0.00s cpu)
[eclipse 46]: Year/Month/Day = 2020/01/01.

Year = 2020
Month = 1
Day = 1
Yes (0.00s cpu)
[eclipse 47]: compound(2020/01/01).

Yes (0.00s cpu)
[eclipse 48]: X is 1/2 + 1/5.

X = 0.7
Yes (0.00s cpu)
[eclipse 49]: set_flag(prefer_rationals, on).

Yes (0.00s cpu)
[eclipse 50]: X = 2020/01/01.

X = 2020 / 1 / 1
Yes (0.00s cpu)
[eclipse 51]: Year/Month/Day = 2020/01/01.

Year = 2020
Month = 1
Day = 1
Yes (0.00s cpu)
[eclipse 52]: compound(2020/01/01).

Yes (0.00s cpu)
[eclipse 53]: X is 1/2 + 1/5.

X = 7_10
Yes (0.00s cpu)

Thus, there’s light at the end of the tunnel :slightly_smiling_face: Or is that a train coming? :stuck_out_tongue:

1 Like

You could use:

P÷Q

Its even a character below 256:

?- char_code('÷', X).
X = 247.

?- code_type(247, X).
X = prolog_symbol ;
X = to_lower(247) ;
X = to_upper(247) ;
false.

On a mac you can type it alt-shift-. .

1 Like