In about exactly 24 hours (including about 8 hours of sleep), I managed to go from having the fleeting thought of ~on~ and ~off~ and ~yes~ and ~no~ isotopes, to implementing the long-wished-for WORD!s-for-logical-states...and having a non-trivially booting system (can run TLS and HTTP, etc.)
The fact that I could do it that fast tells you something--Rebol doesn't really use a whole lot of variables that are true
and false
. It's rare to pass them as arguments to functions, because parameter-less refinements are more the way to do that.
It works better than what we had before. And the ways in which it is "weak" are really just bringing true and false into the realm of how you deal with every other thing in the language. Trying to give an exceptional experience of using LOGIC! has really just wound up as an experience in creating a lot of exceptions. This is a better path, that gets people in the rhythm of thinking with words--not the ideas they are used to from other languages.
Some Spots Showcase The Benefit
Consider a case where you are trying to discern a refinement that has been supplied, and would hold a logic if so:
===: func [
return: [nihil?]
'remarks [element? <variadic>]
/visibility [logic?]
<static> showing (false)
][
if not null? visibility [showing: visibility, return nihil]
if showing [...]
]
You have to specifically say if not null? visibility because you can't just use if visibility to ask if the refinement had been supplied. When you consider NULL to be the only branch inhibiting state, the question is natural... if visibility means exactly that: are you non-null.
===: func [
return: [nihil?]
'remarks [element? <variadic>]
/visibility ['yes 'no]
<static> showing ('no)
][
if visibility [showing: visibility, return nihil]
if yes? showing [...]
]
To me that paints a sort of picture of why we are better off without the likes of #[true] and #[false]. When you get away from the mental chains of thinking you have to be limited to those words, you are getting into the mindset of how to do more with the language.
Some while [true]
Type Places Need A Different Answer
true
is no longer defined, so you can't do that (unless you define true yourself for some reason).
Although technically you could just change that to while ['true]
and it would work, that's rather misleading, because while ['false]
would do the same thing. Not a good way to teach people how to think about this.
You could say while [true? 'true]
and be accurate, but that hardly seems like the best choice here.
while [#]
isn't the worst choice... the zero ASCII character literal is compact--looks solid. It's what no-argument refinements currently use to be branch-triggering. Certainly there's nothing overtly wrong with it, besides being "symboly"... to where someone might look at that and go "what does that mean?"
More talkative choices like while [#forever]
communicate more without costing any more. That's a short enough character sequence to fit in a cell. You could say while [<forever>]
with a TAG! that always does a string allocation (because it's mutable), but it's still a trivial cost when only one copy of the code exists. And it doesn't have to dereference the string node to know it's not a null, so same runtime property as anything else. If you think it looks better I wouldn't worry about the extra 8 platform pointers it costs.
Similar arguments apply to how some people (who don't like generalized ELSE) are accustomed to writing:
case [
condition1 [...]
condition2 [...]
true [...]
]
You wouldn't want to change that to 'true
for the reasons I've already mentioned (e.g. that 'false
would do exactly the same thing).
A literate choice would probably be mentioning how this is some kind of default.
case [
condition1 [...]
condition2 [...]
#default [...]
]
Again, the "default" fits entirely into an immutable character sequence cell such as that, so it's very cheap.
Anyway: my point is that 'true
now represents a particularly poor and counter-educational choice for arbitrary truthy things. There's an infinite number to choose from. Just don't choose that.
Turns Out I'm a 'yes
Man
For practically everywhere that was using true
and false
, I think yes
and no
read better.
is-dynamic: false
=>
is-dynamic: 'no
And they're shorter. Plus if you said if not is-dynamic before, you can say if no? is-dynamic now.
I just wish there was a better name for to-yes-no, if you want convert values that are NULL to no and every other non-void thing to yes.
I am very tempted to reclaim DID and DIDN'T for this. "Did you find it?" -> yes, no
>> did find [a b c] 'b
== yes
>> did find [a b c] 'e
== no
>> didn't find [a b c] 'e
== yes
Predictably Most Questionable: NOTHING for "Branch Trigger"
The likely best other choice would be the zero ASCII character representation, #. It's already used for no-argument refinements when they are supplied.
>> 10 < 20
== #
>> 10 > 20
== ~null~ ; anti
If we went with this, it has the seeming appealing property of letting you store the results of a logical expression in a variable, and then use it without having to invoke YES? or TRUE? or OFF? or anything like that:
>> var1: 10 < 20
>> var2: 10 > 20
>> if var1 [print "This works."]
This works.
>> if not var2 [print "So does this. What's wrong with it?"]
So does this. What's wrong with it?
What's wrong is that I think this would encourage people to be representationally lazy, and it would slip the meaning of NULL into a haze.
You'd stop really knowing what NULL means. It ceases to be "unassigned thing that you don't really want to be passing around as a parameter". People would start passing it around as if it was some kind of measurement. It would become a currency for booleans, and I don't think it should be doing that.
That's why I speak of this "forced triage": I want people to be turning it into the right words for the problem, not leave it as null. And it seems to me the best way to do that is to pick an ornery truthy state.
"Okay, let's say we buy into that: having people assigning "logic" expressions to variables getting them unset half the time is actually a call to action to make better code, and keep NULL living to its full potential. What are the downsides?"
Returning "Logic" (NOTHING or NULL) is a Bit Weird
foo?: func [return: [logic?] ...] [
if whatever blah whatever [
return ~
]
return null
]
That doesn't look a whole lot like a function that's dealing in LOGIC?.
We're in a bit of a pickle in a sense, because we have to find some way of saying "return branch trigger" or "return branch inhibitor" without using the words true or false.
The first idea I had was to make something that we might consider useful for other reasons: LOGICAL, a routine which converts 0 to NULL and all other integers to NOTHING.
foo?: func [return: [logic?] ...] [
if whatever blah whatever [
return logical 1 ; e.g. NOTHING
]
return logical 0 ; e.g. NULL
]
It's not a useless function, and it dodges words we don't want to use. Doesn't seem like a terrible idea.
Of course, a lot of the time you're not generating the states. You're just passing on the result of another function (e.g. writing a routine like even?
in terms of odd?
, and you can just say return not odd? number
without having to generate anything.)
Forces Functions Like REMOVE-EACH To Pick A Keyword
The function REMOVE-EACH will remove the items you say to remove.
red>> remove-each num [1 2 3 4] [even? num]
== [1 3]
Red considers UNSET! (nothing) to be truthy, and REMOVE-EACH only cares if your branch comes out truthy (not specifically true or false). So if your block of code ends with something like PRINT, that winds up being a removal instruction:
red>> remove-each num [1 2 3 4] [print num]
1
2
3
4
== []
I find that a bit too liberal and easy to make mistakes. I required your loop body to evaluate to a LOGIC! (remove if true, keep if false), NULL (keep), or VOID (keep).
If I were to carry that forward...it would mean taking NOTHING, NULL, or VOID. But there are a -lot- of ways to make NOTHING (e.g. PRINT).
Does this mean REMOVE-EACH needs to force you to pick a word form of logic?
>> remove-each num [1 2 3 4] [even? num]
** Error: REMOVE-EACH expects TRUE or FALSE (or VOID to skip)
>> remove-each num [1 2 3 4] [bool even? num]
1
2
3
4
== []
I'd almost rather it expect the body to evaluate to KEEP or REMOVE, but you lose some elegance. Though being able to skip voids might work out:
>> remove-each num [1 2 3 4] [if even? num ['remove]]
1
2
3
4
== []
So we're in a situation where I'm not uncomfortable with the canon branch-triggering state being the same state that unsets variables. That's a feature. The thing I'm less pleased about is if it's being generated by other things (like eval [] or case [1 < 2 []] or print [...])
#
May Be Better Than Too Much Prescriptiveness
It would be consistent with unused refinement arguments. You would be able to use them directly:
>> f: make frame! :append
>> f.line: 1 > 2
== ~null~ ; anti (valid state for the /LINE no-arg refinement)
>> f.line: 1 < 2
== # ; valid state for the /LINE no-arg refinement
(It used to be that we had to worry about whether to convert TRUE and FALSE automatically, but now we would not do that--because those are WORD!s.)
It would allow you to save conditional expression tests in variables, even though you probably shouldn't do that very often.
It would mean that REMOVE-EACH could require #, NULL, or VOID.
But one thing I don't like about it is that you can put it in blocks. I'd rather whatever this truthy state is be an antiform. That's probably a good property for the used refinement state as well.
What's a good short word that's the opposite of NULL that could be branch triggering that isn't TRUE? ~used~
makes sense in the context of refinements, but does it make sense for comparisons in general?
>> 1 > 2
== ~null~ ; anti
>> 1 < 2
== ~used~ ; anti
There's ~okay~
>> 1 > 2
== ~null~ ; anti
>> 1 < 2
== ~okay~ ; anti
Using that for refinements doesn't necessarily seem like an improvement.
>> f.line: if some-condition [okay]
>> f.line: if some-condition [~used~]
>> f.line: if some-condition [apex] ; apex: ~apex~
Apex is actually pretty cool. But certain to look strange.
>> 1 > 2
== ~null~ ; anti
>> 1 < 2
== ~apex~ ; anti
The somewhat pejorative nature of TRASH makes me a bit reluctant to use it.
>> 1 > 2
== ~null~ ; anti
>> 1 < 2
== ~
But also, you need to quote it. I think this all just points to #
being the most sensible choice, even though it's not an antiform. You can't have everything.
NULL Complement Aside, The Rest Is A Winner
I am happy with using the words and the TRUE? YES? tests...and having the new understanding of NULL being the only branch-inhibitor (besides, probably, NaN).