Blocks vs Variadics as Dialect Formats

Imagine you have a function that takes one BLOCK! and emits the data from the block to a file by default. But optionally there's another filename into which the data will be put. The conventional way to do optional arguments in Rebol is via refinements:

emit [x y z]
emit/into [x y z] %foo.txt

The usual suggestion to someone who finds this kind of thing cumbersome might be to consider taking a dialected block:

emit [x y z]
emit [%foo.txt [x y z]]

And if you're lucky enough to know that the slot where X lives can't be a FILE!, you might be tempted to eliminate the nested block. But you may not be lucky...and if the block lives in a variable instead of a literal, introducing a new level of block suppresses evaluation and causes you all kinds of grief. You start needing to COMPOSE or REDUCE, and this kind of problem comes up a lot.

With variadics we now have another possibility:

emit [x y z]
emit %foo.txt [x y z]

At the interface level, rather than implementing this using a single VARARGS! it could be supported more cleanly by marking parameters as <skip>able:

 emit: function [file [<skip> file!] block [block!]] [...]

This could tell the evaluator that if it can't match the type of the first argument, then just set it to void and keep going. An upside to this is that it would mean people using APPLY wouldn't have to jump any particular hoops to build a VARARGS!, and could supply parameters as normal.

Is permitting this a good idea? Well, the cat's out of the bag as variadics are permitted now, and I think variadics have many justifiable applications. The real worry is if you start writing things like:

 emit x
 blah-blah

...and the reader can't be confident if x is a FILE! or a BLOCK!. Of course, if you don't know if x is a FUNCTION! or a BLOCK! you've always had a similar amount of trouble. You also get in a bit of trouble the day you decide that first argument to emit can be either a FILE! or a BLOCK!, still taking a BLOCK! as the second argument. But changing function interfaces always influences callsites, this is just taking it further. I'd argue it's just how Rebol is.

Now this doesn't mean it's a good idea to use for exposing core functionality. But if one is scribbling out a quick-and-dirty script, this freedom may be all part of the deep lake.

1 Like

I like it, and as it is possible, people will do it.
So having it made clear in the function signature is much cleaner.

2 Likes

Looking quickly at implementation details, there is a problem in the issue of parameter convention. You have to have the parameter gathering convention match the thing you're rolling over to. Consider:

foo: function [file [<skip> file!] 'word [word!]] [...]

x: does [print "side effect" [a b c]]
foo x

The attempt to fulfill the file parameter won't quote, it will evaluate... and it cannot undo that evaluation, despite rejecting the argument. Basically the parameter classes must be the same. Soft quotes can only roll over to soft quotes, tight evaluations only to tight evaluations, etc.

Technically speaking, some quotes could roll over into evaluated slots. It's not totally obvious to see how to write that just now.

You also have to roll over to some required parameter before you run out of arguments. Because otherwise, you can potentially break iterative DO/NEXTs on a block acting the same as DO on that block...since there's nowhere to hold the rolled-over piece.

Well if you can do a clean implementation, then great. Even if we have to explicitly evaluate variables first to expose their type!

While I haven't exactly figured out how to write it cleanly without messing up the evaluator loop, it is technically plausible to roll over a quoted argument into a non-quoted argument. Moreover, I actually think this will be the least obscuring case.

The reason it is the least obscuring is because foo blah blah blah blah vs. foo <some-influencing-tag-instruction> blah blah blah blah has the advantage of being clear at the callsite, as opposed to foo mystery-thing blah blah blah blah. Sometimes mystery-thing might be a tag and sometimes it might not.

If I were to be prescriptive, I'd say you should only roll over quoted args, most likely into evaluated ones. If you evaluate you are in a bad situation in the sense that the same code--written with a certain arity assumption--could sometimes skip and sometimes not. It's hard to imagine code making sense in that case.

Perhaps then <skip> should only be available to hard quotes. But rolling over such hard quotes into subsequent evaluation is not something technically obvious how to do cleanly, so I'll have to think on it.

I'm not sure if <skip> is the best name for what this is. But I wound up doing an implementation for it:

https://github.com/metaeducation/ren-c/commit/5857740aedb3bfbafad5dc3c110cb0fab8f34b7d

I didn't do it for frivolous reasons, I did it because it was needed to keep code from breaking that used the new DEFAULT, e.g. case [... default [...]].

What happened is that ELIDE had been showing various weaknesses due to requiring that it run along with whatever was on its left hand side. This kept giving unpredictable ordering problems. At one point, it required changing CASE to essentially quote its branches. While it may not have been that big of a loss, it did create a rift between what a list of IFs could do and what a CASE could do...so it was a little uncomfortable.

Eventually it seemed this really was a problem with how the evaluator was handling functions like ELIDE, and a better solution was needed. That gave rise to a reworking of Eval_Core() and related routines to rethink DO/NEXT that could leave ELIDEs pending for the next evaluator step, and yet not lose the last evaluative product. That interface is now called EVALUATE

This was a relief to all the many situations in which ELIDE had been presenting ordering problems. It also unchained CASE so that the old non-literal-branch functionality could come back, with ELIDE working as one would expect it to:

https://github.com/metaeducation/ren-c/commit/8af4214b985bf7323167b959727592d7b3f842c2

But sadly this broke DEFAULT's enfix-vs-non-enfix polymorphism, which I'd become rather attached to. With the evaluator run on case [... [...] default [...] the block on the left of default would now be visible to it. (It hadn't been when it was lit-quoted, but Eval_Step() runs enfix).

If the trick was to be preserved so that x: default [...] could pick up the X: as well as use DEFAULT in this case, there needed to be some way for DEFAULT to reject whatever was on its left that wasn't a SET-WORD! or SET-PATH!. It couldn't wait to do this rejection until it was running (the way a variadic might) because that would be too late for the block to be bounced back to use as a branch.

The only idea on the map which would solve this would be skippability--with rules adapted to let a left enfix with a type it wanted to skip be deferred until the next evaluator step. While it might feel a bit convoluted, it doesn't feel mechanically "unsound"...any more so than any of the rest of Rebol is unsound. :slight_smile: