Isotopes and NaN (Not a Number)

Prior to the existence of isotopes--when only VOID and NULL existed as outliers--I had an idea that VOIDs and NULLs could act as the quiet NaN and signaling NaN forms of "not-a-number" (NaN). The goal of this is to allow math handling to be more graceful, without needing to set up TRAPs and such--you can be selective about which operations you are willing to have fail, and supply code to fill in such cases.

Original Idea: Math Ops Follow VOID-IN-NULL-OUT

This was the proposed behavior for NULL as signaling NaN and VOID as quiet NaN:

>> square-root -1
== ~null~  ; anti

>> maybe square-root -1
== ~void~  ; anti

>> 1 + square-root -1
** Error: + doesn't accept NULL for its value2 argument

>> 1 + (square-root -1 else [10])  ; selective handling
== 11

>> 1 + maybe square-root -1  ; propagation
== ~null~  ; anti

But I Don't Like VOID as Quiet NaN (and It Breaks Compares)

I prefer that when void arguments are received by math functions, they return the other operand (with the exception of void divisors)...not a noisy NaN.

>> 10 * if 1 > 100 [20]
== 10

Also, Wikipedia has a little table about how NaNs work with comparisons:

Comparison between NaN and any floating-point value x (including NaN and ±∞)

  • NaN ≥ x => Always False
  • NaN ≤ x => Always False
  • NaN > x => Always False
  • NaN < x => Always False
  • NaN = x => Always False
  • NaN ≠ x => Always True

Look at that last case. If VOID is the quiet NaN, you can't have that comparison returning NULL, because it would be falsey instead of truthy...so VOID-in-NULL-out breaks down here:

>> 10 != (1 + try square-root -1)
** Error: != doesn't accept NULL for its value2 argument

>> 10 = (maybe 1 + maybe square-root -1)
== ~null~  ; anti  ...not a LOGIC!, but it is falsey, so uh...perhaps? :-/

>> 10 != (maybe 1 + maybe square-root -1)
== ~null~  ; anti  ...okay, now that's just wrong.

(I've made similar observations about VOID-in-NULL-out with LOGIC! returning functions before.)

Beyond not liking VOID for quiet NaN, I'm not hot on NULL being the noisy NaN either.

New Idea: Signaling is Error Antiform, Quiet is ~NaN~ Antiform

A lot of design space opened up with the introduction of generalized isotopes.

So I think signaling NaN should be an error antiform. And quiet NaN should probably just be the ~NaN~ WORD! antiform.

TRY typically converts antiform errors to NULL, so they don't promote to abrupt failures. But if TRY converted the specific NaN error antiform to the ~NaN~ word antiform, we could avoid having to come up with a special word for "MATH-TRY".

(This makes me think that quiet ~NaN~ word antiform should be falsey, so the usage pattern can line up with other null-bearing TRY instances...and I imagine MAYBE should also turn quiet ~NaN~ to VOID, for similar reasons.)

Anyway, here's how this would play out:

>> square-root -1  ; unhandled raised error return promotes to abrupt failure
** Error: Not a Number

>> try square-root -1  ; TRY intercepts raised error before it abruptly fails
== ~NaN~  ; anti

>> 1 + square-root -1
** Error: Not a Number  ; was promoted to abrupt failure before + could see it

>> 1 + try square-root -1  ; TRY makes quiet ~NaN~, then + propagates
== ~NaN~  ; anti

>> 10 != (1 + try square-root -1)
== ~true~  ; anti

>> 10 = (1 + try square-root -1)
== ~false~  ; anti

>> 1 + ((try square-root -1) else [10])
== 11

>> 1 + any [square-root -1, 10]
** Error: Not a Number

>> 1 + any [try square-root -1, 10]
== 11

I Think That Looks Solid

Another day, another success story for isotopes. Definitionally raised error antiforms provide the mechanics to make an interceptible error by contract as a return value that isn't itself an abrupt failure, but promotes to abrupt failure when not anticipated. :atom_symbol:

And it's cool that we have another WORD! antiform state that has the properties we want, while not being able to be put in blocks... but which has a quasi state that can, if you need to pipe things around:

>> try sqrt -1
== ~NaN~  ; anti

>> block: append [1 2 3] try sqrt -1
** Error: append doesn't accept ~NaN~ antiform as its value argument

>> block: append [1 2 3] meta try sqrt -1
== [1 2 3 ~NaN~]

>> block.4
== ~NaN~

>> 10 + block.4
** Error: + doesn't accept ~NaN~ quasiform as its value2 argument

>> unmeta block.4
== ~NaN~  ; anti

>> 10 + unmeta block.4
== ~NaN~  ; anti

And heck, raise ~NaN~ could produce the error antiform, bringing things full circle.

1 Like

Red has added non-signaling NaN support:

red>> sqrt -1
== 1.#NaN

red>> 10 = sqrt -1
== false

red>> 1 + sqrt -1
== 1.#NaN

red>> 1 = sqrt -1
== false

red>> 1 != sqrt -1
== true

It's considered to be a FLOAT! type, and is truthy:

red>> type? sqrt -1
== float!

red>> any [(1 + sqrt -1) 1020]
== 1.#NaN

But they don't have a signaling form.

Historical Rebol2 and R3-Alpha do not support NaN at all.

Old Discussion on NaN From The R3-Alpha Issue Database

It also wanders into topics like positive and negative zero and infinity.

my view of the nan/inf arithmetic · Issue #1902 · metaeducation/rebol-issues · GitHub

BrianH says:

The main problem is that for people doing scientific calculations Inf and NaN are values that are expected, and which they have been trained to deal with and require in some cases.

However, for people not doing scientific calculations (i.e. regular programmers), those are values that they need to avoid. (...)

You might notice that these are conflicting requirements. This is why scientists often use different programming languages than the ones that regular programmers use, or special versions of the regular languages, or special libraries for the regular languages. And this is why the vast majority of programmers don't use those languages, or versions, or libraries.

So, what would be the best way for REBOL to support those conflicting requirements?

Feel like I've found the best way. :slight_smile: Though you could ADAPT your math functions to squash the definitional NaN errors on their inputs without needing to do TRY if you want.

As for the rest of the discussion, it's more than I want to tackle right now. But maybe @bradrn has ideas on whether we'd need ~+inf~, ~-inf~, ~+0~ and ~-0~ antiforms...

There's another rule, which is that one NaN can't equal another NaN. That's easy enough to throw in.

However...

Once you META or REIFY a NaN, you get an ordinary quasi-word part. The special handling won't apply.

So meta-NaN would equal a meta-NaN.

>> block.4 = block.4
== ~true~  ; anti

>> (unmeta block.4) = (unmeta block.4)
== ~false~  ; anti

A bit weird, but, that's the way this particular cookie bounces. :cookie:

Haven’t read the rest of the discussion, but these things (as well as the NaNs) shouldn’t be antiforms. They should be numbers — ordinary floating-point numbers, as defined by the relevant standards. Making them a different type will become an exercise in frustration, as floating-point numbers suddenly start turning into other types unexpectedly.

So you're saying Not-a-Number should be...a number? I thought you were all about logic and consistency in terminology. :slight_smile:

red>> sqrt -1
== 1.#NaN

red>> number? sqrt -1
== true

Well, I'm more intractably committed to the definitional error part than what the defused errors get turned into. But still feeling pretty confident about the worth of the antiform NaN...

  • I like the clean notation.

  • I like the falseyness.

  • I like that you can't casually put NaN in blocks (but you have means to meta-represent them if you consciously need that...and it makes sense this handling would closely align to how ~null~ and ~void~ and ~ are dealt with).

  • I like that functions which take floating point numbers wouldn't take these by default unless they said they did, e.g. arg [~NaN~ float!]

  • I like that functions which don't specify return: [~NaN~ float!] can't return a NaN.

    • Currently we wouldn't be able to stop a function from returning the Signaling form of NaN. Because we do not limit the definitional errors that are legal for a function to return--any function can raise any error. But we should be enforcing contracts...and breaking the contract should trigger an abrupt "you broke the contract" failure. Maybe something like FENCE! in function specs could list the IDs of the errors that were legal? return: [float! {not-a-number}]
  • I like that before you PRINT or WRITE or otherwise do anything to propagate these values, you get a little speedbump where you need to triage the antiform--because these are exceptional states that don't occur in 99% of correctly functioning Rebol programs.

  • I like the idea that--as with historical Rebol--trying to LOAD a NaN float literal like 1.#NaN will give you an error, because 99% of the time that is likely representative of an unexpected condition upstream in your input vs. an intentional desire to give you a FLOAT!. It's another place where forcing triage is preferred. Once switched to a ~NaN~ quasiform it is no longer a FLOAT!, forcing conscious handling with UNMETA or DEGRADE.

Those attributes would not be accomplished with a floating point literal form and plowing along with other languages' status quo.

Anyway...we'll have to look at practical examples. I'm pretty sure my idea is a good path to take. Though as I say, I don't use Rebol for math (or really anything for math, the kinds of programs I tend to wind up working on don't need a whole lot of it).

In that case, a suggestion: allow antiforms of INTEGER! and FLOAT! to act as definitional errors too. That gives you antiform NaNs ‘for free’, and I can see applications beyond that too (e.g. for representing return codes).

It would be a nice representation, if only IEEE 754 hadn’t already defined them as valid floating-point numbers. It seems very wrong to me that floats with the same representation should be split up between two (or more) different types.

I don't consider myself beholden to IEEE 754, any more than I consider myself beholden to ISO 8601, or ISO 3103. Rebol is a rebellion, after all. :teapot:

Review my list of benefits above (UPDATED).

:point_up:

Remember: this is coming from a place where Rebol2 and R3-Alpha made the decision to simplify programmers' lives by guarding them from the nasties altogether. You are the sort of person clearly who would prefer to go all-in (though your biases may change with time...)

But this is a middle ground. I'm sure there'd be someone out there would be mad when their spew of IEEE 754 literals in a text file from another language--with crazy states--didn't load without tweaking. Yet someone else out there is going to be glad they didn't just get streaming wild numbers into their program, that math blithely tolerated and caused baffling results. I'm pretty sure most Rebol programs are going to be in the latter category--which is what drove the original decision to prohibit the states.

"I don't know what the secret to success is. But the secret to failure is trying to please everybody."

Strongly agree. This is why only the antiforms would be legal.

They could still BINARY! convert as their IEEE representation (or textually convert):

>> sqrt -1
== ~NaN~ ; anti

>> to binary! sqrt -1
== #{FFF8000000000000}

Though I've never cared for the implicit choice of encoding on that. It's why I like ENBIN and DEBIN.

So I'd rather that be something like:

>> encode @IEEE-754 (sqrt -1)
== #{FFF8000000000000}

(Note that I am a stickler for not conflating quasiforms with their antiform state, e.g. I would not allow encode @IEEE-754 '~NaN~. I consider being committed to not doing such conflations to be a critical aspect of isotopic design. Though I can't control what people do in their own functions, and maybe someone has a good reason to do it...but I'm doubtful.)

Automatically converting the IEEE 754 notation to the ~NaN~ quasiform in loading blocks is clearly bad mojo. It's not only injecting an unexpected type for what you might have thought was a FLOAT!, the quasiform is not semantically an actual NaN until you unmeta it. So I'd say that 1.#NaN is simply an error as far as the scanner is concerned (which is the case historically and today). But the error could guide you to the existence of ~NaN~ if you wished to go that route... and if the literal wasn't actually the by-product of some error or unexpected condition (which I imagine 99% of the time it would be).

A Red Sorting Issue Provides a Further Case Study

qtxie says:

You cannot sort NaN as it's not equal to any float values, and neither greater nor lesser than any float values. NaN - Wikipedia

hiiamboris says:

I suggest that sort should treat NaNs as if these were of different type than floats. Although it's a design decision that should be taken by the core team only.

Gregg says kind of what I say above:

The question for me is not what IEEE says, but what makes Red b) produce the most useful result that is easy to reason about¹ (e.g. NaNs group together and other values are sorted correctly), and a) not look stupid.

Beyond that, they have a bunch of bugs that arise when some given function (e.g. max) isn't consciously written to handle the NaN or other anomalous states. The bugs they've logged aren't the only ones they have--merely the ones someone tripped over and complained about--it's a systemic problem.

Rebol2 and R3-Alpha avoided the problem by just not having the representations. But if they're going to exist (and I think they likely should), it seems clear to me that it's better if a routine that takes FLOAT! not be getting these states unless their typecheck interface says they can handle them. A parameter typecheck error is better than just doing something random or wrong.

Antiform word! ~NaN~... and definitional error! NaN... working together... have cross-cutting benefits to sorting this all out. I believe similar arguments apply to the ~+inf~, ~-inf~, ~+0~ and ~-0~ cases, assuming that supporting them does more good than harm.

1 Like