How does it implement and, or, or if?
How does it implement and, or, or if?
https://ryelang.org/meet_rye/basics/if_either/
The tricky thing about if, and, and or --- the reason you can't implement them as functions in most languages --- is that they need to not evaluate all their arguments immediately. Otherwise:
// Would print!
if(false, print("oops!"))
// Would throw an error if the key is not present
and(my_hashmap.has_key("key"), my_hashmap["key"])
The way that ryelang gets around this is that you pass the arguments in a "code block" surrounded by "{}", which delays its evaluation. So you write: // Does not print, because *if* never runs its code block arg
if 0 { print("oops!") }
// There's no example of *and* anywhere but my guess is you'd write this:
and { my_hashmap.has_key("key") } { my_hashmap["key"] }Like Lisp’s QUOTE which has to be a special form.
[1] https://maxima.sourceforge.io/
[2] https://feb.kuleuven.be/public/u0003131/WBT23/wxMaxima/wxM_i...
[3] https://feb.kuleuven.be/public/u0003131/WBT23/wxMaxima/wxM_i...
[4] although (in my limited experience) maxima sometimes refuses to evaluate anyway even if you do the double quote thing
The code block approach is widely applied in two massively used industrial languages though: Ruby and Kotlin. In Kotlin specifically it's one of the very central features.
Anyway, if you want something perhaps a bit more used, you can check out Pharo. I think that is the most used Smalltalk-like language these days.
The way things are done now (and have been for a long time), is that values are stored in a
struct Tcl_Obj{
int refCount; // objs can be shared
int myType; // indicates whether currently a long, double, etc
long longVal;
double dblVal;
[...]
char *stringRep;
int len;
}
...in fact, the Tcl_Obj is more sophisticated than this, but for demonstration purposes this is fine.So "native" (eg: longVal) values are used when appropriate, no marshalling back/forth between strings, but the string rep is always available (can be generated), because that's what Tcl promises: everything is representable as a string. This is what brings the homoiconicity to Tcl - logically it's just passing text tokens around, and emitting text tokens. Internally, again, more sophisticated, but you get the point.
loop is a function that also accepts two, integer for number of loops and again a block of code.
Even fn that creates functions is a function that accepts two blocks, first is a list of arguments and second is a block of code. There is no big difference between function fn and function print, they are both builtin functions defined in same manner and there are multiple fns for special cases and you can create your own on "library" level.
It looks like that's how Rye does it as well, blocks can be conditionally evaluated: https://ryelang.org/meet_rye/basics/if_either/
"In REBOL, contrary to Lisps, blocks or lists don’t evaluate by default. For better or for worse, this little difference is what makes REBOL - REBOL." https://ryelang.org/meet_rye/basics/doing_blocks/
It's difficult for a language with this semantics to be made efficient, but efficiency isn't everything.
That makes it potential interesting to me.
It is interesting to imagine inverting the sense of execution, but as you say it's hard to do much optimization.
In a language where functions don't do that, they're just functions. Rye appears to be one of those languages. One could quibble about whether that's the right thing to call them, but if we say they're vau or whatever, "everything in Rye is a vau" is still true. I think calling them functions is reasonable though.
Since code-is-data, you can swap the if and else clauses around. This applies to all the functions where you've inserted the if statement.
This kind of thing makes it difficult to compile a function to a fast set of machine instructions with the same effect. Not impossible, but difficult.
You can't even assume `+` means "add the arguments", because `+` may have been bound to something completely different prior to evaluating the expression containing `+`.
Turns out the language does have special forms, which is OK; it's just weird to say there aren't (though it's an understandable goal).
When I worked on 3Lisp (so many decades ago) it became clear to me how many special forms there are (a small number, but more than I thought) and, honestly, how few there really are, so the "benefit" of a 3Lisp turns out to be negligible in practice. Oddly enough I didn't really notice that when writing interpreters because I thought of most special forms as simply compiler hacks ("eventually we can get rid of this").
Rye's evaluator seems more complicated, but the forms are regular. A block is always evaluated the same way doesn't change how it's evaluated based on what the first element is.
You can have lambdas without special forms in Rye because blocks aren't evaluated eagerly.
Of course I could be way off; I've been having fun this morning poking at Rye for the first time and my Lisp / Scheme exposure is limited to Uni classes eons ago and a resulting allergy to parentheses.
Seeing the meta-circular Rye would tell us for sure :)
True for function calls. But not for the zillions of macros. The "small number", you mention, are the small number of special operators. But there are many more macros. Those get the arg source unevaluated and return a Lisp form, which then is checked again (either by the compiler or at runtime by a source interpreter).
I'm having trouble parsing this. The two parts there seem to be saying opposite things. Was that an accident, or were you saying that from one point of view it seems to be a lot while from another point of view it doesn't, or something else?
Also, of course, in 3Lisp you can run code in your interpreter and so define new control structures and such. Turns out there aren’t many interesting ones and they have mostly already been thought of.
One new control structure that didn’t need to modify its own interpreter was method combinators. Turns out they mainly useful for unpredictable behavior, except in very simple cases like :before and :after.
So if your built-in functions were implemented in Python, you'd use:
def if_(cond, t, e):
if cond:
return t()
else:
return e()and(x, y) -> bool
or(x, y) -> bool
if(cond, funcTrue, funcFalse) -> void
if(and(or(cond1, cond2), cond3), effect1(), effect2())
In most languages, if `cond1` evaluates to true, you would not evaluate `cond2`. If `cond1` and `cond2` evaluate to false, you would not evaluate `cond3`. If all conds evaluate to false, you would not evaluate `effect1()`, and if `cond3` and either `cond1` or `cond2` are true, you would not evaluate `effect2()`They're not functions because they don't evaluate their arguments before evaluating their body. Their operands are passed verbatim and evaluated explicitly by the body on demand.
In Kernel, for example, we can define these as operatives, which don't evaluate their operands. Assuming we have some primitive operatives `$cond`, `$define!` and `$vau` (the constructor of operatives), and an applicative `eval`:
($define! and
($vau (lhs rhs) env
($cond ((eval lhs env) (eval rhs env)
(#t #f)))))
($define! or
($vau (lhs rhs) env
($cond ((eval lhs env) #t)
(#t (eval rhs env)))))
($define! if
($vau (condition consequent antecedent) env
($cond ((eval condition env) (eval consequent env))
(#t (eval antecedent env)))))
These aren't the definitions Kernel uses in its standard environment. It uses recursive definitions of `$and?` and `$or?` which take arbitrary number of operands, and `$cond` is defined in terms of `$if`, which is primitive: ($define! $cond
($vau clauses env
($if (null? clauses) #inert
($let ((((test . body) . rest) clauses))
($if (eval test env)
(apply (wrap $sequence) body env)
(apply (wrap $cond) rest env))))))
($define! $and?
($vau x env
($cond ((null? x) #t)
((null? (cdr x)) (eval (car x) env))
((eval (car x) env) (apply (wrap $and?) (cdr x) env))
(#t #f))))
($define! $or?
($vau x env
($cond ((null? x) #f)
((null? (cdr x)) (eval (car x) env))
((eval (car x) env) #t)
(#t (apply (wrap $or?) (cdr x) env)))))It's doing if(cond, effect1, effect2) where effect1 and effect2 are functions, and only evaluating the matching effect function. But everything is functions.
Everything being a function is trying to say that every "active word" (a word that does something ... print, first, if, fn, context, extends, ...) is just a function.
fac: fn { x } { either x = 1 { 1 } { x * fac x - 1 } }
; function that calculates factorial
So presumably "either" is the "if" expression.