Thanks for the heads-up. Having my morning caffeine and so I guess I can briefly look at this. But before I can make any sense of the GUI, I need to survey the language a bit.
Three List Types?
It looks like Rye has {...}
which it has called a "block". And despite not being present in any of the examples it presumably has (...)
. But the GUI examples also show use of [...]
.
I can't find what it calls the bracketed form--the web page is not super informative. The web console crashes on nearly every example, and pretty much anything I type. Going to look at the source...
[...]
is apparently a BBLOCK. Bad naming, but at least he calls (...)
GROUP.
Ren-C wins here with [block]
•
(group)
•
{fence}
BUT since all three types exist, it does mean the GUI has more parts. So maybe there will be some ideas about how to leverage that to borrow from.
(I can guess why {...}
was likely chosen as the basic inert type... to look more appealing to Go programmers. But I'm not sure that's a win--standing out is better to know when you're switching languages. Certainly would have been nice to call that a FENCE instead of a BLOCK.)
Falsey Zeros
if 0 { print "Moon" }
; doesn't print anything
0 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.
BBLOCK For Datatypes? No...
I managed to get the console to answer a type question, and it gave back what looked like a "BBLOCK". So I wondered if Rye had gone to a system like &[type]
in Ren-C.
x> type? "abc"
[Word: string]
But, no. TYPE? returns a WORD!. It's just stylizing its output.
x> type? type? "abc"
[Word: word]
:LEFT-SET-WORD Enables BLOCK-as-Lightweight-Function
You will [see] why later, but Rye also has set-words that take value from the left.
"Jim" :name 12 + 21 :apples + 100 :fruits
So "Jim" :name
as a synonym for name: "Jim"
?
This is bizarre, but I dug around and found out why:
loop 3 { :i , prns i } ; loop function injects loop number into the
; code block and left set-word can pick it up
So this is Rye's version of repeat 3 i -> [print i]
, where in Ren-C you would use a GROUP! generally and it wouldn't come out looking too awful comparatively...same # of characters with his example when put side by side:
repeat 3 { :i , print i }
repeat 3 (i -> [print i])
I'm not a fan of the :i
set-word-from-the-left concept. But it is true that Ren-C's approach costs you the generation of a fairly lightweight lambda, vs. efficiencies that could be had by using a stylized block directly.
When I've thought of stylizing blocks, I've never thought of "leading GET-WORD!s". I threw around ideas more like:
repeat 3 [[i] print i]
repeat 3 [i | print i]
Though these days we could glue the variables onto the block with a sequence?
repeat 3 i:[print i]
for each [1 2 3 4] [a b]:[
print ["A is" a]
print ["B is" b]
]
It's ugly...but I don't find it as ugly as Rye's method, and still gets the variables with the block in one slot without creating a function...if that is a goal.
With EACH and such aiming to become generators, this does cut at the heart of some questions about whether we would want:
for each [1 2 3] ... code and possibly var go here ...
for var each [1 2 3] ... code goes here ...
for-each var [1 2 3] ... code goes here ...
Ren-C's approach here just comes out looking better to me:
for [a b] each [1 2 3 4] [
print ["A is" a]
print ["B is" b]
]
for-each [a b] [1 2 3 4] [ ; legacy wrapper, maybe in the box, maybe not
print ["A is" a]
print ["B is" b]
]
You get a variable-naming slot you might not need:
handler: [a b] -> [
print ["A is" a]
print ["B is" b]
]
for # each [1 2 3 4] handler/
But I feel like I can live with that, especially since you might want to use the slot as a filter spec for what to pass...even if you don't need to name things:
handler: [b] -> [
print ["B is" b]
]
for [_ #] each [1 2 3 4] handler/
; prints 2, 4
Anyway... status quo wins here for me.
Failures vs. Errors
Failures can be [handled], code errors can only be fixed
Rye distinguishes between a failure of a function to produce a result, which can be handled nicely, without breaking a flow. Or a code Error, which means an unknown state, so the program should generally stop and Error should be fixed in code. An unhandled failure is also a code Error.
So this sounds on the surface like it may arise from an understanding that reacting to abrupt errors is a bad idea, and you can only handle error conditions at the boundary between caller and callee.
But there is nothing in the Failures and Errors section
I'd be very surprised if it scratches the surface of what definitional errors are doing.
"Context Paths"
We can use a context path, another literal value type in Rye, to address values inside a context. Evaluator uses cpath in the same manner as it uses words. If a word in a context is bound to a function it evaluates it, if not it returns the value bound to it.
print pos-printer/codepage ; prints: w1250
So he uses backslashes for refinements, and forward slashes for picking things out of contexts. It's almost like the :
for CHAIN! refinement vs. .
for TUPLE! picking distinction.
Of course I think Ren-C's decisions are superior here. But notable that refinements and member selection are done with different "path" (sequence) types.
Context Protection
No direct changes
We can’t use the equivalent of set or mod-words, to change the values inside a context. If you come from Rebol, this is possible there, but Rye tries to keep a much tighter control on modification in general. Rye only allows direct changes in the current context, but not in sub-contexts and not in the parent contexts. You can only send messages i.e. make function calls there.
What you can do, is “send messages” to other contexts, that means “call functions” there. But in Rye a limited number of functions that change values in-place (hence can change state) need to end with “!” (exclamation mark), so the calls to such modifying functions are visible and explicit.
count: 0 ; prints: 100 actuator: context { loop 100 { inc! 'count } } reseter: context { change! 0 'count |if { print "Changed!" } } print count ; prints: 0
That is pretty heavy-handed.
I'm sure there's usefulness to a policy of controlling access, and saying only some kinds of references can mutate some fields. Definitely worth thinking about, but even as a C++ programmer who believes in abstraction, I think "if a struct will work, use a struct". There are different granularities of concerns.
I get mad when people look at small Ren-C examples that look awkward and miss the bigger picture, so I don't want to do that here...but...initial impression, I don't think this is a way I would want to be forced to work.
Rye Dialect
Do
is a general Rye function that does the Rye dialect. A
I'll just point out that Carl was adamant that when you write something like for-each 'x [1 2 3] [print [x]]
you are not using "the DO dialect" or "the Rebol dialect".
He said that was distinct from dialects, and was "the Rebol language", in which you implement dialects.
In Ren-C, there might now be grounds for something called "the DO dialect"...in terms of what DO does when you pass it a block... which will not be EVAL. But when you run code, is that the "EVAL dialect?"
I'm certainly not as strongly opinionated on the matter as Carl was. But in deference, I'm willing to just call it "the evaluator".
Extends Object
Function extends accepts another context in addition to a block of code and sets the accepted context as a parent to a newly created one.
Even though I've long hated make (object) [...]
because it put an object instance where a datatype would be, I hadn't really thought of extends (object) [...]
as the generalized replacement for that. Not sure why I haven't replaced it by now.
Think I'm going to follow Rye's lead here. EXTENDS will be how you extend object, not MAKE.
That's all going to need review in light of how FENCE! winds up being used. Lots of thought needed there.
Context Inheritance
Rye is context oriented language, where words get meaning in specific contexts, and we can construct contexts in a way that makes the most sense. In code below context
circle
isn’t viewed as a subclass of math, but the programmer decided that it makes sense to define context circle with context math as the first parent.rye .needs { math } ; check if we have math module built-in circle: extends math { circuference: fn { r } { 2 * pi * r } area: fn { r } { pi * r .sq } } print circle/area 10 ; prints: 314.159265 do\in circle { print circuference 10 } ; prints: 62.831853
I'm a bit confused because I saw another example on the homepage...
; Rye like Rebol or Lisp has no operator precedence.
; But it has Math dialect which has it and more.
math { 2 + 2 * sqrt ( 12 + ( 24 / 3 ) ) }
This makes it look like MATH returns a number. So EXTENDS would be extending the product of a calculation?
PURE Functions
If you probe builtin functions in shell you will see that most of them show something like
[Pure BFunction ...]
. A function being Pure means that it has no side effects and that it has referential transparency. This means that with the same inputs it will always produce same output.Function for creating pure function is pfn. Pure functions can only call other pure functions (builtin or normal).
So I've been meaning to add this (first mentioned in 2020). Especially given that typechecking is done through constraint functions. We don't want to do a typecheck...have a value pass the check and then be marked as not needing to be typechecked again...and then have the function return a different result for the same input on the next call. (The word bindings in the typecheck block also have to be permanent/hardened.)
There's just been bigger fish to fry. But when I did make it, it wouldn't be a special generator. I would just write it as x: pure func [...] [...]
. I wasn't thinking of it as a property of the function... but rather a property of the reference.
The thing is--we can't pre-emptively analyze a function to know if it's pure or impure--we can only catch it when it tries to do something impure. Given that reality, why not let you exploit your knowledge that a given parameterization of a function is pure? It might only be impure when you call it in certain ways.
Or maybe the person who wrote EVEN? forgot to make it pure, but it was:
>> run pure even?/ 10
== ~okay~ ; anti
That might be fine enough for when it came up, but PURE might even be one of these tricky "I interpret my CHAIN!" functions, so you can modify the call:
>> pure:even? 10
== ~okay~ ; anti
Then IMPURE could let you switch out of pure mode to do impure things.
impure [print "Logging!", append log [a b c]]
Seems very fun, just haven't gotten around to it.
BFunction Is "Builtin Function" ("Natives")
probe ?join
prints: [Pure BFunction(1): Joins Block or list of values together]
Just for reference on what that is, in case I see this BFunction term again.
Pipe Words and Op Words
Now we will call the same functions with the same arguments, but we’ll use op-words instead of regular words. Op-words begin with a dot. In case of an op-word, first argument is taken from the left.
"Call me" .print ; prints: Call me 100 .inc ; returns: 101
Pipe words behave similarly or in simple cases exactly the same. So you will get the same results in these examples if as we got them with op-words:
"Call me" |print ; prints: Call me 100 |inc ; returns: 101
Two different ways of saying it, but the pipe word does what Ren-C calls "postponement":
Op-word applies to the first value it can on the left, and pipe-word lets all expressions on the left evaluate and then takes the result as the first argument.
Ren-C enfix has "normal", "deferred" (one eval step, like THEN and ELSE use), and "postponed" (evaluate entire left hand side).
I would accomplish this desire with an operator:
1 + 2 + 3 |echo ; what Rye would be like
1 + 2 + 3 |> echo ; looks fine to me, for cases where I would use this
This reminds me of why I had |>
as a postponing SHOVE operator, it was intended to meet this desire. I never used it.
@earl always said that he considered infix to be a slight compromise that should not be pushed on too much... that the language was fundamentally prefix. I've pushed the boundaries a little bit (type of), but it seems that Rye has decided that infix to accomplish piping is the mecca.
I've already said my piece on this: I don't think it looks very good, and I certainly wouldn't waste lexical space on two ways to say it. I wrote FLOW in a couple of minutes and like it better.
Okay, Survey Complete For Now
Now I know enough that I can read the GUI examples and know what's going on.
It has PURE and three list types, calling (...)
GROUP!. So at least we appear to agree on something. Oh, and EXTENDS too...I need to get rid of MAKE on object! instances.
The context inheritance may warrant a more detailed analysis, but I'd need to build and run it...because the web console just doesn't work.
Red hasn't accomplished anything notable in a long time, IMO (besides some of @hiiamboris's stuff). Rye "deserves" to be there as much as anything.
Where Rye has the potential to be most notable--I'd think--is leveraging Go's infrastructure. Here's some of that:
rye/examples/goroutines/channels.rye at main · refaktor/rye · GitHub
There's also one of the examples in the GUI of using the go
keyword to start a goroutine. So that's probably some of the more interesting stuff to look at.