Thinking About Isotopes Logically: ~true~ and ~false~

Here's an idea: what if ~true~ and ~false~ are examples of weird isotopic exceptions.

Let's say you could assign them to variables...and fetch them from variables...without erroring:

>> true: ~true~
== ~true~  ; isotope

>> false: ~false~
== ~false~  ; isotope

>> obj: make object! [y: true, n: false]
== make object! [
     y: ~true~
     n: ~false~
]

(not being quoted in the assignment indicates they become their isotopic forms.)

This is typically not legal...accessing a WORD! isotope from a variable like true would be an error. But these would be special (like how I'm suggesting isotopic ACTION! would be special in running the action from a word reference...)

Things that tested for truthiness would treat them as expected, considering the ~false~ isotope to be a falsey thing:

>> any [true false]
== ~true~  ; isotope

>> all [true false]
; null

Being isotopes comes with a constraint...you couldn't put them into blocks. You'd have to ^META them (which would lose their special status, turning them into QUASI-WORD!s...that were truthy.

>> ^ false
== ~false~

>> if ^ false [print "Meta false is truthy"]
Meta false is truthy

The functions TRUE? and FALSE? could then work on isotopic, non-isotopic, or word forms of TRUE and FALSE...but error if passed other values.

  • This should work for casual cases of just having variables that hold a true or false state...and gives a good appearance for knowing what you're looking at.

  • It builds on standard mechanisms, and hence if you need to promote the isotopic state into something that can be represented in a block you can do so... with the caveat that once you do, it will no longer reflect its logical property when used with IF and such.

    • That's true for NULL being ^META promoted and put in blocks as well.

And crucially: Since you know isotopes can't be in blocks--and if we say BLANK! is truthy as well--then you'd be guaranteed to visit all the items in a block with code like this:

while [item: try take block] [
    print mold item
]

This is easily the best idea so far for dealing with the problem.

I just booted an isotopic true/false system... and...

YES! Isotopic ~true~ and ~false~ is The Answer (for the Core)

Note that I don't think it's up to me to say whether any particular use case wants an IF statement to react to things. The whole idea is that you can rig it up however you want.

To pick an example: Some people may have a very good reason to have their IF consider integer 0 falsey when writing certain kinds of code. I think that being able to "skin" the system in usermode so it does that--and runs that code in the same session as modules which don't do that--is a good goal to shoot for.

But the system needs a firm foundation for making those variations, and to hopefully inspire people not to change it when they see why the choice was made. And from what I'm seeing so far, this really does feel like it:

>> true
== ~true~ ; isotope

>> false
== ~false~ ; isotope

>> if true [print "Works!"]
Works!

>> if false [print "As expected!"]
; void

There's huge benefit to isotopes not being able to be put in blocks...it forces some kind of triage.

>> compose [a (1 = 1) b]
** Script Error: Invalid use of ~true~ isotope

The tendency of historical Redbol to render #[true] and #[false] and #[none] indistinguishable from the WORD!s true, false, and none was not an accident: it was a crude attempt to limit the leakage of literal values that people often did not want to see. But the results were outright misleading.

Here we get cued into a choice of how to shape our structures. For instance: if you want the QUASI!-WORD! of ~true~, you can do that by reifying it:

>> compose [a (reify 1 = 1) b]
== [a ~true~ b]

Or if you wanted the WORD! true, then you can go that direction as well:

>> compose [a (as word! 1 = 1) b]
== [a true b]

And if someone thinks they have a really good reason to default one way or another, they can customize it. But to me, erroring has the right level of agonsticism for the core.

Fitting into that balance is that ~true~ and ~false~ are the only isotopes that the core IF and friends will accept, so you still get a decent amount of safety:

>> ~xxx~
== ~xxx~  ; isotope

>> if ~xxx~ [print "other isotopes still error"]
** Script Error: if needs condition as ^META for ~xxx~ isotope

The ~true~ and ~false~ QUASI!-words remain themselves truthy, for all the various good reasons they should be truthy (as the WORD!s true and false are as well).

>> first [~false~]  ; getting unevaluated out of block, so not isotopic
== ~false~

>> if first [~false~] [print "plain unevaluated ~false~ is truthy"]
plain unevaluated ~false~ is truthy

>> decay first [~false~]
== ~false~  ; isotope

>> if decay first [~false~] [print "decayed ~false~ is falsey!"]
decayed ~false~ is falsey!

There are measurable feature benefits to having IF and CASE and ANY defaulting to testing an ANY-VALUE! like that for "is-ness". It solves many puzzles of trying to author generic block manipulation code cleanly.

It's a Morale Boost On A Top-Of-The-Page Problem

For years I've scrolled past the first steps of the boot process, and in the very early boot it has a bit that does an initialization of global cells holding true and false literals.

So right up when you step into main() you hit an essay about how #[true] and #[false] kind of suck, no literal notation has been chosen, along with all the historical question of the duality between the always-truthy true and false words with the literals...

It has been a chronic bummer to see it and lament that "we don't even know what to do with true and false!" But here is an answer that looks and functions better than anything before it.

It's hard to say when things are "fully solved" but this is feeling darn near close. Which means the nest major step in the puzzle will be being able to achieve those customizations I speak of... e.g. to make INTEGER! of 0 falsey, but not have to rewrite every logic-oriented function in the system to do it...

3 Likes

This interferes with an attempted "safety feature" in MATCH, where it previously tried to protect you from yourself:

 >> match [logic!] true
 == #[true]

 >> match [logic!] false
 == ~false~  ; isotope

The idea was that an "isotopic false", through its ornery-ness, would stop you from writing things like:

>> value: false

>> if match [logic! integer!] value [print "Runs if match... not!"]
** Error: IF does not accept ~false~ isotope as its condition argument

Now we're talking about a world in which the isotopes aren't just friendly to conditions--they are the currency of "LOGIC!" in the first place.

But in this world, logic is no longer an ANY-VALUE!, so there's no "LOGIC!" type. I've thought that maybe isotopes would look like [~word!~] if appearing in a typeset, though that has contention with the idea of having a representation for typechecking a QUASI-WORD! specifically (you'd only have the generic QUASI!). Moreover that would be more general than just ~true~ and ~false~, so you'd need a constraint like [<logic>] to restrict a parameter to just those two isotopic words.

New NULL MATCH Reasoning with VOID-in-NULL-out

The safety mechanism in MATCH for NULL used to use null isotopes as well, with a similar reasoning:

>> match [<opt> integer!] null
== ~null~  ; isotope

(Yes, <opt> is slated to be turned into <null> at this point.)

Now we're in the situation where void is the "null isotope", so this isn't how it's done anymore. Instead, NULL is ^META'd to a BLANK! and put in a parameter pack:

>> match [<opt> integer!] null
== ~[_]~  ; isotope

This makes it react with THEN and not ELSE, while still being fundamentally "nully":

>> (match [<opt> integer!] null) then [print "This works, which is cool"]
This works, which is cool

But once again... it's friendly-and-falsey where IF and other conditions are concerned:

>> if (match [<opt> integer!] null) [print "Matched, but this won't run."]

This Is Probably a Make-Your-Own-Safety Case

It's good--(and not bad!)--that I worry over this kind of stuff enough to run through the ramifications!

BUT.

We can't cripple the fundamental mechanics to implement some notion of "safety", when safety is far from the raison d'etre of the language.

If you want safety, you can get it through the vision of customization...in R3C or R3Whoever:

 match: enclose (augment :match [/unsafe]) func [f [frame!]] [
     if f.unsafe [return do f]  ; unchecked variation
     let result': ^ do f  ; run the MATCH, assume it packs matched falsey answers
     any [
        result' = '~[~false~]~  ; e.g. result is false isotope in a pack isotope
        result' = '~[_]~  ; e.g. result is null in a pack isotope
     ] then [
        return raise "MATCH produced falsey product, use /UNSAFE if intended"
     ]
     return unmeta result'
 ]

That adds a refinement to MATCH to get its original unchecked behavior (with AUGMENT), and then wraps it up with an extra check if you use the refinement...guarding it from returning a falsey result otherwise.

I think this is the best way to look at it.

And I actually imagine a good majority of people who would put up with the language as a whole would consider this to be unnecessary.

2 Likes

It's worth a shot to contemplate if this could be another thing tackled with isotopic objects...maybe?

With an isotopic object we're not forced to give back something that resolves directly. (We can even stop it from resolving at all without some kind of mitigation--if we wanted to.)

The object could handle conditionality in the abstract without ever needing to define what true or false "is". An object with a THEN method could be thought of as truthy... and one with an ELSE method can be thought of as falsey...but you don't have to use those words.

It seems this could cover things like if 1 < 2 [print "Less"] -- because IF just says it's interested in isotopic objects and wants to communicate with them, vs. force them to reify into a value. Should it get an isotopic object it would look for a THEN method. If it didn't have one it would look for an ELSE method and if so reject it. If it had neither, it would look for a REIFY method and test the result for NULLness as a last resort.

...But What Would A Logic Variable Be, Then?

So then you have the question of what this does:

>> var: 1 < 2
== ???

Or this:

>> var: true
== ???

Isotopic objects are ephemeral, and not supposed to be stored in variables--at least not at all casually. They exist to fill in representational gaps.

Building on the proposed "assign methodization", we could say that the isotopic object gets a unique pass to write the ~true~ and ~false~ isotopes into the variable they are assigned to. But that these would error on ordinary access.

>> var: 1 < 2
== ~true~  ; isotope

>> var
** Error: var is ~true~ isotope

Then we could give you special accessors for dealing with logic variables... true? and false? These would detect specifically the true/false isotopes and close the loop...giving you back isotopic objects that could be used conditionally... and which could rewrite the ~true~ and ~false~ isotopes into variables if need be.

>> var: 1 < 2
== ~true~  ; isotope

>> if var [print "You'd get an error on the isotope"]
** Error: var is a ~true~ isotope (use TRUE? or FALSE? to test)

>> if true? 'var [print "This would print"]
This would print

(Maybe IF TRUE? VAR would would work through some kind of variadic-quoting-magic that allowed it to suppress an error on accessing an isotope via VAR)

What Would Be The Benefit Of All The Runaround?

It could avoid the special treatment of ~true~ and ~false~ isotopes somewhat, in that they wouldn't be themselves truthy or falsey.

You get safety to protect you against forgetting to use TRUE? and FALSE? on your "logic" variables. (Imagine if you just used the words TRUE and FALSE to represent logic, but then forgot to use the helpers... you'd run into if false-var [print "This would run"] situations constantly.

On the surface it feels like it might abstract truthiness and falseyness in such a way that your conditional wrappers could make 0 seem like it was falsey if you wanted to. But you could only make an object that was conditionally false that resolved to 0 when it wasn't asked about its conditionality. That wouldn't work so long as IF is going to be seeing all non-null non-isotope values as truthy if they're not wrapped...and it would be truthy after the reification (with no protection as offered by isotopic true and false).

Well, It Was Worth a Try To Talk About

All told, it seems simpler to just let a ~true~ isotope be truthy, ~false~ be falsey, all other word isotopes be neither conditional true nor false. And then make isotopic words an unusually friendly sort of isotope.

>> 1 < 2
== ~true~  ; isotope

>> var: 1 < 2
== ~true~  ; isotope

>> if var [print "Maybe a little quirky, but comprehensible."]
Maybe a little quirky, but comprehensible.

This lets people use logic variables the way they are used to... with a reasonably readable representation. You're only forced into trouble spots when you probably should be applying extra thought--like when you want to put a true/false state into a block.

So I am pretty gung ho on this. It has the feeling of a right answer. As I've said, it was always really depressing to start reading the boot from the top and have a big essay on "we have no idea what logic literals are"... and now not only do we know, but there's a whole deep philosophy on the reified realm being truthy vs. the isotopic realm having some falsey things. It's high-leverage, high-IQ stuff. :brain:

The question of just how friendly to make these isotopes runs up against some issues.

For instance, what about MOLDing? In the bootstrap, we have for instance a debug flag which can be set to things like on or normal or symbols etc., but when it's set to ON in the default config it actually becomes a ~true~ isotope.

So that now causes an error, since MOLD refuses to turn an isotope into a word:

print ["debug:" mold app-config/debug]

Historical Redbol is willing to wordify the logic:

red>> mold on
== "true"

I think this intermixing of words with logic is flawed, and I actually like how it gets caught here. The code which is assigning debug: on in the configuration is better as debug: 'normal.

For other cases, we've got REIFY if you are okay with ~true~ and ~false~... and it passes through non-isotopic things as-is:

>> reify "hello"
== "hello"

>> reify true
== ~true~

If you want a word from something you know is a logic, there's LOGIC-TO-WORD

>> logic-to-word true
== 'true

Overall there are a lot of issues like this, but I think that we want to keep MOLD and PRINT unwilling to do isotopes. It's a little bit of a speedbump, but helps you get involved to make sure the right thing happens.

2 Likes