Simplicity When Combinatorics Aren't Less Dangerous

In case I haven't said it enough (mostly to myself), I'll say it one (?) more time: calling API functions which splice in values is very much like DO COMPOSE.

 REBVAL *result = rebValue("some code", value1, "more code", value2);

acts like:

 result: do compose [some code (value1) more code (value2)]

This sets you up for pain when value1/value2 are evaluative types and you did not intend them to be evaluated.

When using COMPOSE in the language, we're lucky these days to have nice generic quoting. If value1 is an ACTION! you want to run, and value2 is a WORD! you want to pass by value... you can apply the quotes where you want them:

 result: do compose [some code (value1) more code '(value2)]

Writing this in the API, you can use rebQ():

 REBVAL *result = rebValue("some code", value1, "more code", rebQ(value2));

Do We Need both rebValue and rebValueQ?

The most foundational and pure API to give people is one that does what they asked. If you wanted a quote, you ask for it. If you don't, the value is left as-is. If it's in a context where you didn't want it to be evaluative and it evaluates...that's just a bug and you need to fix it.

I resolved to make operations like rebValue, rebElide, rebUnboxInteger etc. not add quotes for this reason. But to try and be "helpful", I thought to introduce auto-quoting versions like rebValueQ, rebElideQ, and rebUnboxIntegerQ that quote unless you explicitly ask for the quote to be removed with rebU().

If you had a lot of rebQ() calls in a given invocation, there was a performance advantage to making the Q-version of the call. And visually you weren't taking up as much space on a line.

But I'm on the fence looking at it in practice. This doubles a lot of the API interface, and now it doubles the doubling because there's C++ wrappers and C wrappers. In an API that was designed to be minimal and elegant, it undermines that elegance by appearing redundant.

One problem is that it adds an element of choice, which is a tax. Every time you make a call you wonder if you used the right one. And when you're in the middle of looking at a line of arguments you always have to scan back and wonder "is this in a Q call or a non-Q call".

Is removing rebValueQ and friends the right move?

There's a perceptual cost to making the API look larger and more cryptic. There's already a lot to learn, and then this cost is something anyone looking at the API or code samples will have to pay.

The performance advantage is fairly negligible compared to the overall cost of scanning/binding an API call. In cases that have no text portions to scan or bind, there's nearly zero advantage (for esoteric technical reasons I won't go into).

I'm not convinced the pain is reduced by that much by offering the alternatives. The main pain you are saved from is that when you have a lot of parameters you want to quote, you're saving characters.

If it seems too verbose to use Q a lot, there's always even more abbreviated shorthands:

#define Q rebQ

REBVAL *result = rebValue("some code", value1, "more code", Q(value2));

And the function of rebQ() actually is not to quote a single item, but to be able to enter "quoting mode" for an arbitrary number of items. It applies to splices, not loaded code from c strings. So if you need it rarely, you could say:

REBVAL *result = rebValue(rebQ("some code", value1, "more code", value2));

Obviously I have some conflicted feelings about this, or they wouldn't exist. But I'm leaning toward killing off the rebValueQ, rebElideQ, etc. variants. Mostly because I think it will make the API and callsites appear more orthogonal to those reading it.

As is often the case, I write something that explains a motivation to be worth it to try something... then I try it...

The biggest practical issue with this pertains to dealing with NULL in the API. One area that NULL now comes up a lot in is unused refinements. So if you're looking at JavaScript code like:

replpad-write-js: js-awaiter [
    return: [<opt> void!]
    param [<blank> text!]
    /html
]{
    let html = reb.DidQ(reb.ArgR('html'))

That's just trying to tell if a refinement was used. It's already a bit of a mouthful...it's actually a shorthand for:

    let html = reb.DidQ(reb.R(reb.Arg('html')))

This is a way of saying that you're not actually interested in getting an API handle for the argument value, you want to (R)elease it as soon as you examine it with reb.Did.

(This points out that JavaScript is extra noisy, because we have to put the declarations inside a module, so reb has a dot after it.)

You might think that since it's a JavaScript NULL you could avoid the Did() test entirely...

let html = reb.Arg('html');
if (html) {  // e.g. testing for null
    ...
}

But now you're leaking API handles. You'd have to write:

let html = reb.Arg('html');
if (html) {  // e.g. testing for null
    rebRelease(html);
    ...
}

Anyway, back to the original problem...now we're talking about no more "Q" functions, so:

    let html = reb.Did(reb.Q(reb.ArgR('html')))

The reason the Q is needed in the first place is that you can't use NULL in the API. e.g. you can't write:

  REBVAL *value = nullptr;
  bool is_null = rebDid("null?", value);

It has to be quoted. If you wonder why that is: imagine that code transformed into a BLOCK! when it wants to show you where the error is. But you can't imagine it.... because you can't put nulls in blocks. e.g. the above is not valid as a stream of code. In the DO COMPOSE [...] example the NULL would just vaporize, which would be very confusing if we did that here.

If you try to be lax about this, it's a house of cards. Things break.

It's better to identify common cases and make some kind of macro for them than to break things foundationally.

So maybe something like reb.HasArg('html') in the API? or reb.IsArgUsed('html')? The latter is more obvious, I suppose, since the refinement must be present on the function for the arg lookup to not error.

This can be tested a bit just by writing it in usermode:

 reb.IsArgUsed = function(name) {
     let arg = reb.Arg(name) 
     if (!arg)
         return false
     reb.Release(arg)
     return true
 }

What I consider most important is that people have a complete set of tools for writing what they need, that works coherently and doesn't have blind spots...vs. having necessarily the "friendliest" API in the core.

1 Like