LANG: 0 should not be a "branch inhibitor"
Hello! Long time no C (mid day or other time 😃)
Was told about your progress on the GUI, via someone sending a hackernews link. You're famous! :-)
I was very pleased to see you had decided on {...} as a list type. I have come to believe this is very important!
(Not sure what state of development you consider yourself to be in, but if you think things are still malleable, calling {...} a FENCE! and [...] a BLOCK! seems really nice, and would help us standardize our terms...)
I think there are promising aspects to using Go as a substrate, I definitely think green-threading and such is good, and would hope to learn from what you find.
But I did do some digging, to try and understand what you are doing:
https://forum.rebol.info/t/rye-language/1768/6
The first thing that jumped out at me, is something I would urge you to go back on:
if 0 { print "Moon" } ; doesn't print anything0 being "falsey" breaks the usefulness of ANY and ALL and such, crippling the ability to write idiomatic code:
value: all [<<condition1>> <<condition 2>> some-number]I can think of a lot of reasons to make it "truthy", and 0 reasons to make it falsey. If picking things to change about the language I definitely wouldn't change this.
Might I convince you to back that out, and, hopefully re-engage the Ren-C design... (which is getting very, very good!)
I'm hoping that Rebol-derivatives might standardize more things, even if they cater to variant audiences (Go programmers, Nim programmers, etc.)
Thanks for your feedback Mr. Fork :). I think your proposal makes sense, and I was always cringing a little when I had to write "Truthy" in documentation or tutorial and thought that this might have to change at some point.
The reason I initially didn't implement boolean true / false values is that I remember from rebol (at least I had some confusion) of what is true ... why does it look like every other word, but it isn't. Maybe it was just me but it seemed inconsistent or at least confusing. I wanted no exceptions if possible so true / false in Rye are just words that are bound to builtin functions that return 1 and 0.
Since I'm not sure if there is really something odd about true / false in Rebol I went trying now, correct me if I don't understand something ...
>> print true
true
>> print 'true
true
>> type? true
== logic!
>> type? :true
== logic!
>> type? first [ true ]
== word!
>> type? first reduce [ true ]
== logic!
Ok, so maybe it does make sense, it looks to me from this that true is a word, like a constant, that is bound to logic value true. The confusion comes maybe that logic value has the same spelling in REPL as the word and that it's easy to mix them up?
Your argument about branch inhibitor makes sense to me. I wasn't thinking about that but yes, it's certanly a source of bugs and limitations and inconsistent at the end. And I want the language to be flexible, but environment and values explicit so I want to go away from all truthines and falsines. Concretely, with this I would redo for example if to only accept boolean and you have to be explicit of what is true / false ... like if is-zero x { } or if is-empty b { } instead of implicit conversions that might or might not make sense in certain situation.
So true and false would still be functions, because I don't have builtin constants in Rye runtime for now. But they would return a "env.Boolean". In Rye console the returned value is printed with type information so it will be [Boolean: true] which would avoid the confusion in repl.
I will read the other links you sent in next days. I see you mentioned goroutines ... if you haven't seen yet, here is some related information: https://ryelang.org/blog/posts/rye-concurrency-go/
About { } and [ ]. Those were the same for a long time, but recently when doing Fyne code I found a to me definitive use for [ ], which is like reduce [ ] in Rebol. I had reduce for hof-s (map,reduce,fold,...) and had problems finding a word for it either way ... I tried evaluate, eval (overused in diff context), vals, valuate, ... I think it's currently vals otherwise and ( ) is basically do [ ] in Rebol terms, this one also behaves as () in Rebol I think and can be used for grouping / priority / etc.
Note please that in Rye every token (also []{}(()) has to be space separated so [1+2] is (multiple) loader error(s). It has to be [ 1 + 2 ], which is probably weird comming directly from rebol (it's more of a Factor thing), but so far I'm staying with it, because I have some conventions for words like (rule) and * at .word* has special behaviour for example.
The reason I initially didn't implement boolean true / false values is that I remember from rebol (at least I had some confusion) of what is true ... why does it look like every other word, but it isn't. Maybe it was just me but it seemed inconsistent or at least confusing.
It's not just you. It is indeed very inconsistent. Ren-C solves this problem (and others) in a fully generic way, using what are called isotopes. There are some states which variables can hold which are not able to be stored in blocks. This has been applied to solving many different problems:
https://rebol.metaeducation.com/t/a-justification-of-generalized-isotopes/1918
So, the "antiform" of the word "null" is the only thing that acts as a "branch inhibitor" in Ren-C. There is a complementary antiform called "okay" if you want a canon "branch trigger" that can't be stored in a block.
But if you want to use words to represent logic...like TRUE and FALSE...you can do so. But IF and friends don't test those. So you would have to say if flag = 'true [...] or if flag = 'false [...]. And there are narrower functions TRUE? and FALSE? which ensure the thing you are passing is only in the set [true false]. Of course this means there's nothing special about those words: I find that YES and NO often make a lot more sense.
This has worked out very well. In places that are user-facing--where a variable's state might be readily exposed--using words makes sense. In other places, like the value of refinements that can only be on or off, easily testing them is more important so they use the ~okay~ vs. ~null~ antiforms. You can convert them with BOOLEAN or BOOL
>> 1 = 2
== ~null~ ; anti
>> append [setting:] 1 = 2
** Error: Cannot append antiform ~null~ to blocks
>> boolean 1 = 2
== false
>> 1 = 1
== ~okay~ ; anti
>> append [setting:] bool 1 = 1
== [setting: true]
I wanted no exceptions if possible so true / false in Rye are just words that are bound to builtin functions that return 1 and 0.
Making conditionals test against a null state (where that null state cannot be stored in a block) has resolved the historically annoying problems. You don't have random failures when writing:
block1: [...]
block2: []
while [item: try take block1] [append block2 item] ; TAKE without TRY errors on empty block
I think it's an important invariant--that should succeed for any state of block1.
Concretely, with this I would redo for example if to only accept boolean and you have to be explicit of what is true / false ... like if is-zero x { } or if is-empty b { } instead of implicit conversions that might or might not make sense in certain situation.
The specific strategy of isotopes (quasiforms/antiforms) is not something you'd have to copy completely. But you could just say some states can't be put in blocks--only held in variables. Though I think that would possibly lead you down the path of thinking that the right "API" for getting at what these forms are is to think of them as variants of plain values.
In practice, I think a lot of things work out better when there is only one branch inhibitor...you can write things like all [x = 1 y = 2] and not worry about the failing state of that coming back as false vs. none/null, because there's only one such state.
Array splicing is one of the many things that work out better if you have a splice type that itself cannot be stored in blocks.
>> replace/all [[a b] a b a b] [a b] [c d e]
== [[c d e] a b a b]
>> replace/all [[a b] a b a b] spread [a b] [c d e]
== [[a b] [c d e] [c d e]]
>> replace/all [[a b] a b a b] [a b] spread [c d e]
== [c d e a b a b]
>> replace/all [[a b] a b a b] spread [a b] spread [c d e]
== [[a b] c d e c d e]
This is getting implemented now. We got a separate Boolean datatype and all / any will only accept false as true false. 0 and empty strings will be truthy in this case and pass. if / either will only support boolean argument so there will not be any hidden rulling on what is truthy and what is not and specific functions will have to be used like is-positive , is-zero, is-empty.
It should arrive with next big update and push to version v0.0.90 as discussed on reddit.
We got a separate Boolean datatype and all / any will only accept false as true false.
Hm, what is the result of FIND if it can't find something?
all [
find "abc" "b"
find "def" "g"
print "This should not be reached, right?"
]
My concept of making the distinction ~null~ vs. ~okay~ ("antiforms") for logic resolves this, as find "def" "g" is null... and 1 = 2 is also null.
The idea that PRINT returns the same thing that an unset variable holds is consistent with historical Rebol, and for a time I thought it might be "truthy" 🤕 though I'm leaning to saying it is "no vote" in ANY/ALL (which is consistent with what Oldes has done) and that you cannot pass the unset state to "ordinary" parameters (hence it's illegal to pass it as the condition of an IF or EITHER...) If the unset state isn't no vote but an error, then you might have to write elide print "..." to "erase the result", I'm still debating the question.
( BTW, 2024 recap if you are interested: https://rebol.metaeducation.com/t/2024-recap-with-great-features-come-great-incompatibility/2362 )
Rye so far doesn't have find like Rebol has it. I think in Rebol there was found? so in this manner your block would be something like.
all {
found? find "abc" "b"
found? find "def" "g"
print "This would return this string in Rye, if reached"
}
But it gets more complicated. Rye has convention, where noun? means get-noun and for boolean results I the idea I am trying is to use is-adjective/noun ... like is-empty, is-zero, is-positive ... But Rye also so far has idea that if something fails to do what you ask it to it should return failure (which is specific) versus null or false (which is unspecific). Rye itself has no null. And failure has to be "fixed", while null or false can be passed on and become a unexpected argument somewhere later.
So I don't have a full answer to your code yet, since vocabulary to work with failures is still being created. You could do something like this if you had find.
all {
fix find "abc" "b" { false } ; or you could write it like
find "def" "g" |fix { false }
print "This should not be reached, right?"
}
But I believe there should be better words for cases like this than fix ... as I said, it's still something I experiment with in practice.
2024 recap looks interesting ... I need to take some time to read it all. Thanks.
2024 recap looks interesting ... I need to take some time to read it all. Thanks.
You might also find it interesting to see my breakdown of the found Rebol 1.0 interpreter and documentation:
https://rebol.metaeducation.com/t/rebol-1-0-retrospective/1788/5
Your issue triggered me thinking about this, and with Rye goal is to be flexible with language composition but strict / exact with data / state. So your feedback made it obvious that we DO need boolean, and
it solves the "branch inhibitor" issue that I didn't think that much before.
Thank you for the feedback!
With this I am closing the issue. If you find anything lacking with this subject please reopen.
Frankly, I've skimmed the links you posted, but I want to read them more thouroughly once when I have time a, andhen I might reply on that.
Error: builtinifrequires argument 1 to be Boolean
Certainly it seems that everyone who ponders Rebol-adjacent designs sees different things worth preserving, or okay to sacrifice. I think forcing injection of a call to convert boolean arguments for conditional tests sacrifices something that makes the language endearingly succinct.
I also believe that taking TRUE and FALSE out of the pool of WORD! isn't consistent with the overall "vision" of this language family.
So your feedback made it obvious that we DO need boolean
As I said, I just unified the "none" state with false... it's called NULL, and while it can be an evaluative product or stored in variables, it can't occur literally in a block. There are things called quasiforms which can be put in blocks, which evaluate to the forms that can't be stored in blocks:
>> first [~null~] ; what you see in the block there is a quasiform
== ~null~
>> append [a b c] first [~null~] ; this is legal
== [a b c ~null~]
>> ~null~
== ~null~ ; antiform (produced by eval, it's like dropping a quote level)
>> null: ~null~ ; definition of NULL (antiform WORD! in variables legal)
== ~null~ ; antiform
>> append [a b c] null
** Error: Cannot append ~null~ antiform to block
>> append [a b c] meta null
== [a b c ~null~]
>> if null [fail] else [print "antiform null is falsey"]
antiform null is falsey
>> if first [~null~] [print "quasiform null is truthy"]
quasiform null is truthy
>> if '~null~ [print "quasiform null is truthy"] ; quote suppresses eval
quasiform null is truthy
~okay~ is a canon quasi/antiform that is the complement to ~null~. So you turn the words TRUE and FALSE or ON and OFF or YES and NO back and forth into these antiforms when they are put in variables whose logic-cue nature is important.
with Rye goal is to be flexible with language composition but strict / exact with data / state
If you examine the antiform/quasiform design you will see that it brings rigor and useful invariants while preserving that "endearing succinctness":
>> source: [~null~ true false 0 _ [a b c] ~]
>> dest: []
>> while [item: try take source] [
append dest item
]
>> dest
== [~null~ true false 0 _ [a b c] ~]
This is guaranteed to move the contents of any block to an identical copy of the original. Such guarantees are good and solid foundations for building on.
TAKE SOURCE returns an antiform error value ("raised") when there's nothing to TAKE, and TRY is able to convert that to a ~null~ antiform before it promotes to an abrupt failure, which is what it would do in the next step by default. And the block can be appended as-is because only an antiform list (antiform GROUP!) will splice, hence you won't find it literally in blocks.
Antiforms have been the foundation of so many solutions to problems and useful features that I think being a Rebol developer and ignoring them is sort of like being a mathematician and ignoring complex numbers:
https://youtu.be/B1J6Ou4q8vE?t=209
Please engage if you have any questions or "don't get it":
https://rebol.metaeducation.com/t/a-justification-of-generalized-isotopes/1918
Frankly, I've skimmed the links you posted, but I want to read them more thouroughly once when I have time a, and then I might reply on that.
While most of the forum is me talking to myself, replies are welcome there when people have anything to say.
Certainly it seems that everyone who ponders Rebol-adjacent designs sees different things worth preserving, or okay to sacrifice. I think forcing injection of a call to convert boolean arguments for conditional tests sacrifices something that makes the language endearingly succinct.
Yes. Rye is not trying to be the next REBOL, but I do think that rebol core ideas are the best foundation to build a language upon. Succint is a positive, exact is also a positive at how I want to use the language.
I also believe that taking TRUE and FALSE out of the pool of WORD! isn't consistent with the overall "vision" of this language family.
True and false are just builtins / constructors that return Boolean true and false values. In the same manner a table, vector, dict and list are constructors for basic Rye values. That's what I didn't get at first. In Rebol I always mixed words true/false with values true/false and didn't know what is what. Maybe it was also all clear and I just didn't have the right picture about it.
Here true and false were previously also functions returning 1 and 0 (Integer), but after your feedback, it dawned on me that they could just the same be returning boolean values that so far (and maybe forever) don't have a literal representation.
About your idea of quasi and anti forms, I have to say I am not sure for now, and I don't even understand it all. Rye so far doesn't have nulls at all. The idea was, can functions always return something meaningful, and if not (if they can't) they return a failure. The reasoning was that failure contrary to Null is specific, holds some information about what happened and in Rye failure also has to be handeled in the next function (some functions are failure handling functions), if not ... it turns to and error (coding error, because you didn't predict to catch a failure so program is in uncertain state now), so program stops and you should fix the code (there is a global error handler in plan and there is a block level try { } also, but that is for specific cases).
1 / 0 |print ; failure becomes and Error and program halts
1 / 0 |fix { 100 } |print ; fix is a failure accepting function and it disarms it and returns 100 in this case
I will read again about your quasi/antiform ideas ... I also occasionally look at your forum, and I like how deep you go on some subjects.
Thank you,
I have to say I am not sure for now, and I don't even understand it all.
I've explained the concept the best I know how. It would be very helpful to me if you cited any parts that you don't understand, as I'm trying to make it as easily as possible to grasp. Point out anything that you get lost over.
>> replace [[a b] a b a b] [a b] [c d e]
== [[c d e] a b a b]
>> replace [[a b] a b a b] spread [a b] [c d e]
== [[a b] [c d e] [c d e]]
>> replace [[a b] a b a b] [a b] spread [c d e]
== [c d e a b a b]
>> replace [[a b] a b a b] spread [a b] spread [c d e]
== [[a b] c d e c d e]