Function Escapable Literal Parameters... :X or ':X ?

Background with GROUP!s (prior to GET-GROUP! existing)

Sometimes you don't pass a literal block of code to a conditional, but the callsite runs code that generates the branch--or fetches it out of a variable.

Historical Rebol has this behavior for GROUP!s in conditionals:

 rebol2>> branches: [[print "Apple"] [print "Banana"]]

 rebol2>> either false (print "one" take branches) (print "two" take branches)
 one
 two
 Banana

Doesn't seem ideal. But having both groups run is the only choice you have if the branches are evaluative parameters. The GROUP!s are evaluated before EITHER has a chance to say anything about it.

I pointed out that this was inconsistent with things like historical CASE:

 rebol2>> branches: [[print "Apple"] [print "Banana"]]

 rebol2>> case [
      false (print "one" take branches)
      true (print "two" take branches)
 ]
 two
 Apple
 == true  ; ignore silly `true` (Ren-C & Red give PRINT's result)

Ren-C Takes Branches Literally and Was Able To Sync This Up

Ren-C had begun requiring branches be single literal values for other reasons, namely to give it access to the quote status of the argument:

ren-c>> if true 'a
== a

ren-c>> if true '[a b c]
== [a b c]

So to provide code you'd have to put it in a group (unless it's a single variable, where you could use a GET-WORD! or GET-PATH!) For technically fiddly reasons, it also means that if (...) else (...) is fully swappable with either (...) (...) when they would not be quite exact replacements otherwise.

So long as we're going this way, conditionals have the choice to see the GROUP! which would evaluate to produce a branch, yet decide not to run it. There were enough :heart: clicked on the idea to support this, so that's what we have now:

 ren-c>> branches: [[print "Apple"] [print "Banana"]]

 ren-c>> either false (print "one" take branches) (print "two" take branches)
 two
 Apple

This goes further into the rabbit-hole of bending parameter conventions to suit the likely intent of usage. But I think it's a good example of where it's worth it. It's better than twisting around to get the reverse consistency...forcing CASE to run its GROUP!s regardless of the condition matching.

Soft Literal Parameters vs. Hard Literal Parameters

Rebol2 introduced the variations of "hard literal" as :X and "soft literal" as 'X. Hard literals would always give you the callsite value literally...

rebol2>> hard: func [:x] [print mold x]

rebol2>> var: 'foo

rebol2>> hard var
== var

rebol2>> hard :var
== :var

rebol2>> hard (first [foo bar])
== (first [foo bar])

Soft literals would make GET-WORD!, GET-PATH! evaluative...with everything else literally what was at the callsite:

rebol2>> soft: func ['x] [print mold x]

rebol2>> var: 'foo

rebol2>> soft var
== var

rebol2>> soft :var
== foo

rebol2>> soft (first [foo bar])
== (first [foo bar])

R3-Alpha added a twist: that soft quotes would evaluate GROUP!s vs. taking them literally:

r3-alpha>> soft (first [foo bar])
== foo

Two big points:

  • This looks backwards for :x and 'x. Shouldn't it be that the thing with GET-WORD! in the parameter name is the one that's "colon-reactive"?

  • The picture gets even more consistent if we use GET-GROUP! for the code escape. Then all the quoting subversion is colon'd, and regular GROUP!s can go back to not being "soft" as in Rebol2.

This makes the branch quoting example work with soft branches. SO you get control if you want to get back to the "run the code whether you take the branch or not" situation:

 ren-c>> branches: [[print "Apple"] [print "Banana"]]

 ren-c>> either false :(print "one" take branches) :(print "two" take branches)
 one
 two
 Banana

I hope all the above makes sense. Please speak up if it does not make sense.

So... Should Escapable Literals Be :param or ':param ?

One truism is that unless you have a very good reason to need to quote GET-WORD!s, GET-PATH!s, and GET-GROUP!s...your construct should permit escaping to make it easy for callsites to tunnel in what they want without resorting to APPLY. This likely guided R3-Alpha to make escapability the default behavior for the common 'param form.

So if we're asking people to default all literal parameter callsites to the escapable convention, then :param is nicer to look at than ':param, even if the latter is "more communicative".

One factor to weigh would be if there were an interesting meaning for :param that didn't involve literalness. Let's review the new escapable literal concept, and pretend for a moment we say it's a quoted get-word!:

 >> soft: func [':param] [print mold param]

 >> var: 'foo

 >> soft var
 == var

 >> soft :var
 == foo

 >> soft (first [foo bar])
 == (first [foo bar])

 >> soft :(first [foo bar])
 == foo

So is there any meaning for "plain escapable"?

 >> huh: func [:param] [print mold param]

 >> huh ... ?

Presumably this would similarly be an instruction to how :x, :x/y, and :(x y) are handled at the callsite. But if you're not escaping the quoting, what are you escaping?

It would be silly to say you are "escaping evaluation":

 >> huh :(a)
 == a

That is done more cleanly with quoting, as a generic "escaping evaluation" tool for evaluative parameters...and doesn't raise questions about how to treat huh :(a b)

>> huh 'a
== a

There are some other choices for what it might mean, like "optional". That's something that doesn't apply to literal parameters (because you can't get NULL literally).

func [:arg [any-value!]] [...]  <=> func [arg [<opt> any-value!]]

I don't know that it makes a whole lot of sense. And having a way to recognize NULL in-band in the type block is important:

 >> match [<opt> integer!] null then [print "This should run"]
 This should run

Brainstorm On One Aspect of Distinction

So trying to make the change I tripped up on some edge cases, which suggests that for some functions they are more willing to let soft quoted sites come from the evaluation of more than one cell.

We might imagine for instance that ':x or :x controls how enfix is handled. Consider the following:

>> integer! = type of 1
== #[true]

>> integer! = :(second [length type]) of 1
== #[true]

For OF, this is how you want the interaction to shake out when you are midstream in an enfix evaluation... integer! = (type of 1). But other constructs might want to consider an in-progress enfix something not to interrupt...and treat it like the left was more-or-less in a GET-GROUP!.

As an example, consider the most vexing evaluation:

>> if true [<branch>] then x -> [print ["THEN" x]] else [print "ELSE"]
THEN <branch>

>> if false [<branch>] then x -> [print ["THEN" x]] else [print "ELSE"]
ELSE

The THEN and ELSE branches want to see their argument literally, e.g. if it's quoted. But it wants branches to be escapable (so you can use the GET-GROUP! to run the branch regardless), and it wants enfix to be able to overrule it. That's different from the left argument for OF, which wants escapable literalness but does not want the enfix equality operator to win.

We might then say that ':x is the strict form of "only one value on the left is inspected". e.g. it is quoted, and then it is escaped. Then :x could mean "it's accepted if it's already in mid-evaluation, otherwise it is quoted, then escaped".

So :x would be saying "I don't out-prioritize enfix" while ':x says "I beat enfix just as 'x would, but then the escaping is automatic so you don't have to do it inside your function body".

Continuing the thought experiement...you'd have SOFT

>> soft: func [:x] [print [mold x], 10]

>> soft add 1 2
add
== 2  ; (soft add) 1 2

>> soft 1 + 2
3
== 10  ; soft (1 + 2)

>> soft :(1 + 2)
3
== 10 

Then MEDIUM

>> medium: func [':x] [print [mold x], 10]

>> medium add 1 2
add
== 2  ; (medium add) 1 2

>> medium 1 + 2
1
== 12  ; (medium 1) + 2

>> medium :(1 + 2)
3
== 10

>> medium (1 + 2)
(1 + 2)
== 10

And HARD would be as you expect, always exactly the callsite parameter.

While I'm admittedly a bit wary of super complex parameter conventions, I also like having full coverage of the potential desires. What worries me a little is that this feels like the "tight" parameter convention coming up again...though it's backwards and maybe a little more logical, saying the only places you can ask for looseness is in parameters that are "weird" by virtue of quoting. While it's late and I don't really have a full articulation of why this is more coherent... I think it likely is.

But...mucking with the code and seeing impact across the system in the tests is the way to test that theory.


Bonus Question... what might :[get blocks] do with escaping?

It seems that if GET-WORD!, GET-PATH!, and GET-GROUP! evaluate for :SOFT parameters, then there might be some kind of magic for GET-BLOCK!s.

>> soft :[what's this for?]
== ???

Even if we can't think of anything right now, I'd think they should error to reserve for future use (vs. pass the GET-BLOCK! through as if it were any other type). Or just to keep from seeming inconsistent.

Hoo boy.

Well, four years later, we have a very interesting meaning for :param... it's now what a "refinement" is. This has shaken the foundations of it meaning "getting", and it now has more of a connotation of "optionality".

For instance, that means it's taking the place of slashed-word in multi-return, to signal that it's okay when you have too few items to unpack:

>> [a b c]: pack [1 2]
** Error: too few items in pack to unpack

>> [a b :c]: pack [1 2]
== 1

>> a
== 1

>> b
== 2

>> c
== ~null~  ; anti

While slashes did look little better when considered in isolation, the new role of paths gives them a much higher purpose in function-related annotations and composition. The overall superiority is readily apparent if you work with it for just a brief time...take my word for it!

So Now... How to Represent Escapability

We need to have leading colons mean "refinement" in function specifications, that is the only choice that makes sense now.

func [arg [...] 'lit [...] /refine [...] '/lit-refine [...]]

func [arg [...] 'lit [...] :refine [...] ':lit-refine [...]]

While nothing is mechanically prohibiting us from using leading colons to mark escaping at callsites, I think it would be a poor choice.

The way I had been thinking was that we would simply use GROUP!s on the arguments, to semiotically suggest the escaping:

func [arg [...] 'lit [...] :refine [...] ':lit-refine [...]]

func [arg [...] '(esc) [...] :refine [...] '(:esc-refine) [...]]

But I'm getting a little bit of cold feet about it now. Does being in a GROUP! remove it too much from looking like it's part of a list of arguments?

There are some weird new available notations. Just spitballing here, but what about:

func [arg [...] 'esc:() [...] :refine [...] ':esc-refine:() [...]] ...

It's not pretty.

My original idea looks good in HELP I think, here's some filler from APPEND to make it look more realistic:

ARGUMENTS:
    arg [~void~ any-series? port! map! object! module! bitset!]
        Any position (modified)
    '(esc) [~void~ element? splice?]
        What to append (antiform groups will splice, e.g. SPREAD)

REFINEMENTS:
    :refine [any-number? any-series? pair!]
        Limits to a given length or position
    '(:esc-refine) [any-number? pair!]
        Duplicates the insert a specified number of times

Contrast that with:

ARGUMENTS:
    arg [~void~ any-series? port! map! object! module! bitset!]
        Any position (modified)
    'esc:() [~void~ element? splice?]
        What to append (antiform groups will splice, e.g. SPREAD)

REFINEMENTS:
    :refine [any-number? any-series? pair!]
        Limits to a given length or position
    'esc-refine:() [any-number? pair!]
        Duplicates the insert a specified number of times

...yeah, that's not very palatable.

Should It Be'(:esc) or ':(esc) ?

(Note that these are the only legal options, as :'(esc) is illegal and (':esc) doesn't have any decoration on the group at all which makes it even easier to mix up as just some regular group from a COMPOSE or something.)

So is it "a quoted escapable refinement" or a "quoted refinement that's escapable"? I feel like the escapability modifies the quote and not the refinement, so '(:esc) seems more "correct".

But looks-wise, I have to say I prefer ':(esc)

func [arg [...] '(esc) [...] :refine [...] '(:esc-refine) [...]]

func [arg [...] '(esc) [...] :refine [...] ':(esc-refine) [...]]

Let's not forget there's a "give me the bound literal" version too:

func [arg [...] @(esc) [...] :refine [...] @(:esc-refine) [...]]

func [arg [...] @(esc) [...] :refine [...] @:(esc-refine) [...]]

Hrmph. I do prefer pushing the colon as far out as I can so the "I'm a refinement" is communicated as early as possible.

I think it's somewhat illusory to think the composition matters here much beyond picking whatever looks best. You've got three properties: refinement or not, normal or bind-literal or unbound-literal, and if literal, escapable or not. Refinement is actually the first thing you need to know but as I mentioned the colon cannot lead the quote mark in this syntax, so... whatever. Encode the 3 properties how it looks best.

It Doesn't Have to Decorate The Parameter Word...

...but it probably should. There's going to be too much business going on in the type checking block, we don't want this stuff:

func [arg [...] esc '([...]) :refine [...] :esc-refine '([...])]

func [arg [...] 'esc [<escapable>...] :refine [...] ':esc-refine [<escapable> ...])]

Pretty clearly gets less good fast.

I think I've talked myself into '(arg) and ':(arg)

I was leaning this way all along, but I think the shift to ':(arg) from '(:arg) actually makes a material difference in terms of how much I like it.