Line Continuation and Arity Bugs: Thoughts?

At one point in time, there was no way to pass something to RETURN that represented a VOID. Because voids completely vanished. If you had a function that took an argument--and a void thing came after it--the evaluator would just keep retriggering until it found a value or hit the end.

This led to the only way of being able to return a void to be to have truly nothing after it. So RETURN became effectively arity-0 or arity-1. If you passed it no argument, it would consider itself VOID. It even had the nice property of being able to chain functions that themselves might be void.

Given that RETURN was doing this, some other functions followed suit. QUIT could take an argument or not. CONTINUE could as well.

But I Just Got Bit By a Variadic QUIT Bug

Without thinking about it, I tried to end some code prematurely by injecting a quit:

 some stuff I wanted to run

 quit  ; added this

 some stuff I wanted to avoid running

And that QUIT ran the stuff I didn't want to run anyway, because it was variadic.

My Kneejerk Reaction Was To Kill The Variadicness

The original case of RETURN has changed, because so-called "non-interstitial invisibility" is dead. You can only make expressions void in their totality--not when used as arguments. Doing otherwise caused more harm than good.

Hence return void is a perfectly fine thing to write (or return ~ if you prefer the quasiform of void, which you probably don't, but it might come in handy somewhere if you've defined VOID to mean something else)

I'd been thinking that argument-less RETURN would thus go back to returning the default unfriendly value (currently called NONE, a ~[]~ isotope, e.g. a parameter pack with absolutely no values in it). But maybe we shouldn't support argument-less RETURN at all.

But Variadics Can Be Neat

I guess RETURN could always take an argument, and we go back to CONTINUE/WITH and QUIT/WITH.

But those are uglier.

We might question the behavior of the system just randomly slurping up arguments from enusing lines? Especially when APPLY has such a convenient notation now, of some-func/ [...]

From a usability perspective, there's certainly a lot of potential for error in getting the arity wrong. Having it be more strict could catch bugs, and make it more likely that variadic arity is being used correctly.

1 Like

I realized this issue is very similar to problems that come up in JavaScript with "automatic semicolon insertion":

JavaScript/Automatic semicolon insertion - Wikibooks, open books for an open world

JavaScript doesn't require semicolons at the ends of lines. There's some debate on whether to take advantage of this--but I'm one of the people who believes it makes the code cleaner. But cases like RETURN have a parallel problem in JavaScript.

So JavaScript's return is variadic, but the automatic semicolon would bias it so that if you have some stuff on a line after a return it gets ignored.

The complexity of the situation makes me feel like erring on the side of fixed arity. If you want to return nothing, say return none. And if you want to return void, say return void. It's more explicit, and I think the "naked" returns probably do more harm than good.

I don't think people will use passing arguments to continue too often, but continue/with value isn't terrible

2 Likes

A design loophole of this is that nulls at refinement callsites (currently) represent refinement revocation.

e.g. if you are to say:

 >> count: null
 == ~null~  ; isotope

 >> append/dup [a b c] [d e] count
 == [a b c [d e]]

Then it's as if the /DUP weren't present at all. That seems sensible enough...

...but it would imply that (continue/with null) is a synonym for (continue).

Yet the /WITH is supposed to be a fancy way of saying "pretend I completed the loop body /WITH this value". To pick one example: you're not allowed to have the body of a MAP-EACH end in null:

>> map-each x [1] [null]
** Error: Cannot append ~null~ isotope to collected MAP-EACH block

In practice we want CONTINUE with no /WITH to act as if the loop ended in a void--not as if it ended in a null.

For that outcome in the world of today, that suggests not erroring when the /WITH is null:

>> map-each x [1 2 3] [
       if x = 1 [continue]
       if x = 2 [continue/with null]  ; must be same as plain continue? :-/
       if x = 3 [continue/with void]  ; seems more synonymous w/plain continue
   ]
== []

It's a bit unsatisfying to have that /WITH of a null not acting like if the loop body ended in null and erroring.

This makes me wonder if refinements should implement some kind of rule that makes null and void synonymous... or coupled in a way reminiscent of void-in-null-out.

Perhaps even at callsites, the null state would be illegal:

 >> count: null
 == ~null~  ; isotope

 >> append/dup [a b c] [d e] count
 ** Error: Refinements at callsite can't be null, use e.g. MAYBE to get void

 >> maybe count
 ; void

 >> append/dup [a b c] [d e] maybe count
 == [a b c [d e]]

What it would mean would be you'd remove refinements from the interface using voids and not nulls. It's a decision kind of in line with how removing elements from MAP! is now done with voids and not nulls, which I called "a hassle, but probably a good hassle"...helping to prevent accidents.

Upshot would be no (non-^META) void refinements

I can't really predict all the weird edges here, but this does suggest refinements can't be received as void in the body of a function in conventional code.

It wouldn't be the end of the road for distinguishing a callsite null from a callsite void with a ^META parameter--though that sounds like a low priority. (See ~end~ for a solution in that vein which would probably work here.)

And as far as frame mechanics go, maybe it would pay to be lenient and allow ~null~ and void as interchangeable ways for getting a null refinement. :man_shrugging:

So refinement revocation is going to be under scrutiny for a while as I look into this.

1 Like