Should END-able constructs all use ^META parameters?

If you had a function that took a literal argument in Rebol2, it could be "endable"... if it accepted UNSET! as an argument.

This feature was added primarily for HELP, so that you could say either:

>> help
...GENERIC HELP usage information...

>> help topic
... HELP for specific TOPIC...

It was a very limited form of variadic-ness...generally used only in console-oriented commands (HELP, LS).

You couldn't use it with a function that was evaluative. Hence this wasn't possible:

redbol>> printer 1 + 2
3

redbol>> printer
You called the PRINTER function with no arguments
    ; ^-- not possible to accomplish with an otherwise evaluative argument!

So handling missing arguments was tied to taking the argument literally.

It was also ambiguous. The signal for literal parameters that were endable-and-missing was to make the parameter accept UNSET!. Which meant they couldn't tell the difference between help #[unset!] and just-plain-help:

red>> help #[unset!]
To use HELP, supply a word or value as its
argument:

    help insert
    help system
    help system/script
    ...

Ren-C Can Resolve the Ambiguity for Literal Arguments

Because antiforms can't occur in "source" code, a function taking a literal argument could receive an antiform as an unambiguous signal that the end was reached.

Any antiform could serve as that signal (and different options have been tried). But for the moment let's say it was the antiform of the word ~end~:

>> help ~end~
~end~ is a quasiform word!  ; received the quasiform of END

>> help
To use HELP, supply a word or value...  ; received the antiform of END

Normal Evaluative Arguments are Still Ambiguous

When a quasiform evaluates, it produces an antiform. So we still would have ambiguity if we tried to use the ~end~ antiform:

>> printer 1 + 2
Received 3

>> printer
Received the antiform of ~end~  

>> printer ~end~
Received the antiform of ~end~  

But ^META Arguments Can Be Unambiguous

Meta-arguments are evaluative, but if the evaluated value is not an antiform, it will just have a quote level added. Antiforms will be passed as the quasiform. This means you can never "legitimately" receive an antiform as an argument to a ^META function.

>> meta-printer 1 + 2
Received '3

>> meta-printer first ['3]
Received ''3

>> meta-printer ~end~
Received the quasiform of ~end~

>> meta-printer first [~end~]
Received '~end~

So a special rule could be invoked that endable ^META arguments give an antiform to signal the missing parameter:

>> meta-printer
Received the antiform of ~end~  

Should Only Literal / ^META Arguments Permit Endability?

The ambiguity for "normal" evaluative arguments only arises if your type checking would allow the antiform. If your function only accepts INTEGER! (for instance) then the antiform of ~end~ isn't "in-band", and it may be more convenient for you to not worry about the ^META convention.

 int-printer: func [arg [<end> integer!]] [
    either arg = ~end~ [
        print "Received end antiform"
    ][
        print ["Received integer:" arg]
    ]
]

That seems okay to me.

The system could try to stop you from shooting yourself in the foot... so that if you marked a normal parameter as <end> it would try to type check an ~end~ antiform against your parameters:

 >> anti-printer: func [arg [<end> antiform?]] [...]
 ** Error: typespec [<end> antiform?] ambiguous for ~end~ antiform

So is that annoying, or helpful?

3 Likes

So I want to talk briefly about this choice for the "end signaling antiform", and how that ties into "default values and make frame!"


Right now, when you do make frame! you get a copy of a frame with all the unspecialized slots (e.g. PARAMETER!) initialized to trash (~ antiforms):

>> frame: make frame! either/
== #[frame! [
    condition: ~
    okay-branch: ~
    null-branch: ~
]]

(Quick reminder that although what's shown there are quasiform blanks, the convention is that you interpret foo: xxx as meaning the FOO field actually holds what xxx evaluates to. So condition: ~ means that the CONDITION field holds the evaluative product of the ~ quasiform, e.g. the ~ antiform... an "unset!" if you prefer to think of it that way.)

The question on the table relates to whether normal evaluative parameters are able to receive "trash" or not. So let's imagine I did this:

>> frame.okay-branch: [print "truthy"]
>> frame.null-branch: [print "falsey"]

>> eval frame
; ...is this an error?  prints truthy?  prints falsey?

In the current paradigm, it's an error:

>> eval frame
** Script Error: either has condition unspecified (~ antiform)

So the rule is that "trash" is the one stable antiform which you can't accept as a normal argument.

If this is truly the case, it would seem like the obvious candidate for signaling an <end>, as the state is already reserved in all cases...

...or is <END> a different intent from Unspecified?

More accurately, the question is: "Even if there's a difference in intent, is it worth it for overall system complexity to have a separate antiform and handling?"

Because clearly when an end of evaluator input is reached, all subsequent argument gathering is also at the end. Whereas unspecified parameters in a frame can occur in any order.

(e.g. I didn't assign the condition field of the EITHER frame above, but then filled in both branches. That's impossible in left-to-right fulfillment to mean "the end was reached at the condition")

Hmmm. :thinking:

Case Study: MAKE FRAME! on Variadic Input

There's an experimental variation of SWITCH that allows you to use partial expressions:

>> switch2 1020 [match integer! => [#i], match tag! => [#t]]
== #i

So here you see an arity-2 function (MATCH) receiving only one argument. And the SWITCH2 fulfills the missing argument:

How it works is that the evaluator is called on the incomplete expression, and then writes an ~end~ antiform into any slots that are at the end:

>> frame: make frame! [match integer!]
== #[frame! [
    test: &[integer!]
    value: ~end~
]]

Today that's conflated: you'd get the same thing if you wrote:

>> frame: make frame! [match integer! ~end~]
== #[frame! [
    test: &[integer!]
    value: ~end~
]]

So it seems like an improvement if the situation was:

>> frame: make frame! [match integer!]
== #[frame! [
    test: &[integer!]
    value: ~
]]

>> frame: make frame! [match integer! ~]
** Script Error: match can't take (~ antiform) at callsite

The deal would just be that you can't take ~ antiforms at the callsite unless the parameter is ^META (which means you'd receive the parameter as a ~ quasiform, still leaving the ~ antiform free for unspecified-ness).

Could Typechecking "Trash" Replace The <end> Flag?

Imagine if a parameter typechecks against the trash antiform, then we assume it is "unspecifyable". So not assigning it in a frame... or reaching the end of an input evaluation... however you do it, it's all right to receive as a trash antiform.

But if it's driven by typechecking alone, then it's actually the ^META parameters that get mucked up. Because if they say they typecheck the trash antiform, they (historically) mean they will receive evaluative trash arguments as meta-trash.

So one of these things would have to be true in such a world:

  1. ^META parameters can't be endable/unspecifyable

  2. ^META parameters which accept evaluative trash conflate it with unspecifyability

  3. ^META parameters can't accept evaluative trash, and typechecking it means unspecifyable

[1] is right out. ^META is an expansive parameter convention when you've hit the limits of what a normal parameter can do. It needs to be strictly more powerful.

[2] and [3] seem to involve unpleasant tradeoffs.

Consider (set var ~) I think that needs to work, because (var): ~ works And SET has to take its value argument as ^META (for other reasons, like skipping the assignment and propagating definitonal errors). But this shouldn't imply that (set var) works.

In conclusion, unspecifyability needs to be a separate flag from typechecking.

What To Call Unspecifyability? <end> Feels Wrong Now

I've been calling refinements "optional" parameters. e.g. ":DUP is an optional argument to APPEND"

>> append.dup.optional
== ~okay~  ; anti

But maybe I should stick with the name "refinement". And this idea of being willing to be left unspecified is <opt>... true optionality. Like, you can literally omit it from the callsite.

It creates a little bit of a problem, because not specifying a refinement in a MAKE FRAME! situation--leaving it as trash--has meant that refinement argument will be set to NULL when the function runs.

But an alternative perspective on MAKE FRAME! would remove the unused refinements:

>> make frame! append/
== #[frame! [
    series: ~
    value: ~
]]

I proposed what has traditionally been thought of as MAKE FRAME! be some other operator. But maybe that operator doesn't leave things as antiform trash, yet rather sets them to null?

>> XXX append/
== #[frame! [
    series: ~null~
    value: ~null~
    part: ~null~
    dup: ~null~
    line: ~null~
]]

This could get rid of the concept that typechecking ever morphs ~ antiforms into anything else. And FWIW, could mean that there could be such a thing as an optional refinement--not that I can think of any actual applications of such a thing.

Devil's in the Details, but...

Regardless of what happens, I do think I like moving away from "endability" to "unspecifyability".

Seems fairly elegant. It's also in line with Carl's "UNSET! is not first class" proclamation:

It's important to understand the unset! datatype; otherwise, we run the risk of assuming that it is first class (assignable, passable, returnable) when it's really not intended for that kind of usage!

...although assignment is one of those "meta" circumstances where it's better to allow than disallow. While comparison and such don't count, though you can "meta-compare" an unset.

>> if ^(print "hi") = '~ [print "meta-comparison works!"]
hi
meta-comparison works!

:face_with_head_bandage:

Due to LIFT the UNIVERSE, ^META arguments no longer leave room in the "unlifted" band for special states to indicate <end> or "unspecialized". Because all variables are stored "lifted", and "unlifted" when fetched. The point of distinction is merely whether a FRAME! gets them automatically decayed before the lift.

So the only way to go any "lower" in representation than what arguments can be is the "dual band"...special states that can't be reached with SET or GET. This is the home of getters, setters, type checkers, aliases... AAAAND "true unset"... the unspecialized state.

So the questions here are:

  1. Do we want more than one true unset state in the dual band, to account for END as distinct from regular "left unspecialized"?

  2. For parameters that can't be antiforms (literal args from the callsite) is it worth it to try and exploit that to use what would normally in-band as NULL to make it easier to test as if not var vs. using unset? $var

(1. No) (2. No.)

It seems to me that just letting the true unset state cover this is the best plan.

If you mark a parameter as <end>-able (maybe not the best name for the attribute... <unset> may actually be the right name for it, now that there's a state that's "not even SET") then it should be tested with (unset? $x)