APPLY II: The Revenge

It's time to bring back APPLY.

First Big Question: Quote The Function or Not?

This has been on my mind since the beginning: should you have to use a GET-WORD! to name the function to apply, or does it quote by default...and make you put code fragments that returns a function in a group.

 ; Worldview #1
 apply :append [series value]
 apply second reduce [:insert :append] [series value]  ; will APPLY :APPEND

 ; Worldview #2
 apply append [series value]
 apply :append [series value]  ; still legal if you want
 apply (second reduce [:insert :append]) [series value]  ; GROUP! is required

(Note: We have at least some relief at not having to consider Worldview #3, of apply 'append [series value] so APPLY can capture the name "append" for debug stack purposes. That's not needed because :append gets an ACTION! that carries the label in its cell. This has been a change that punches above its weight.)

In the beginning I would have said it definitely has to go with Worldview #1, because in the second case you don't get a clear signal that APPEND is not being called on [SERIES VALUE] and the result being passed to APPLY. If you didn't see the apply or were otherwise distracted, your reading comprehension would be thrown off by breaking from convention.

Over time I've become more of a believer that the whole premise of the language is to let you bend constructs to predominate use, so the 99% case is pleasing to use. The now famous case of OF's left-quote, for instance:

type: 'type of x  ; without OF quoting

type: type of x  ; with OF quoting

The quote mark makes it clearer, but when OF becomes so common that you "just know" then it's a much more pleasing function to use. How many times do you use OF with a reflector you pick via code? When is that code a single-arity function you wouldn't have to put in a GROUP! to use with enfix anyway??!

But does that same philosophy to APPLY, that we come to read apply append as one indivisible unit, and feel thankful for not having to reach for the colon key? It's a little bit different than OF because you read left to right, and you can start reading at the APPEND with no speedbump after it.

I'll stress that you would still have the choice. If you were the type of person who thought it clearer to use the colon, you could do so. You just wouldn't be able to invoke a function-to-generate-the-function unless you put that in a GROUP!.

I can't make up my mind. So I think I'm going to make the answer Worldview 1.5:

 ; Worldview 1.5
 >> apply second reduce [:insert :append] [series value]
 ** Error: APPLY arguments must be GET-WORD! or GROUP! at the moment

This leaves the door open to interpreting plain WORD! as naming a function in the future. And it gives time for people to find a really compelling case for where not being able to generate a function without using a GROUP! would be a deal-breaker.

Naming Refinements

Historical APPLY had a nasty positional nature for refinements, which led to things like this, from the R3-Alpha module code:

return map-each [mod ver sum name] source [
    apply :load-module [
        mod true? ver ver true? sum sum no-share no-lib import true? name name delay
    ]
]

You see the combination of true? ver followed by ver as filling in first the refinement slot for /VERSION, and then the value of the refinement argument.

Then you see "used or not" refinements being supplied to their argument cells by the value alone (no-share signaling not to provide /SHARE, no-lib signaling not to use /LIB)

But we can now put refinement names in the APPLY dialect!

apply :load-module [
    mod /version ver /sum sum
        /share null /lib null  ; avoids need for "clarifying" `no-share: false`
        /import import /name name
        /delay delay
]

You might ask what was stopping that before. The issue was that historical Rebol had the problem that if you had an argument that could take on any value...including NONE!...then NONE! could not be used to represent the state of the refinement itself.

(Imagine if you are specifying an initial value to fill a block with (as in ARRAY/INITIAL). It may be important to distinguish putting a NONE! in vs. not having a default at all.)

This is solved in Ren-C by reserving the non-valued NULL state for "refinement not taken".

Note: I'd gone back and forth a while over whether a REFINEMENT-like PATH! would be appropriate to use in this dialect, based on the question of whether they would be evaluator-active or not. Ideas were floated around like foo would fetch a variable but be an error if it was NULL, while /foo would fetch a variable and allow NULL.

In the end, inertness for refinements won out. BLANK!-headed PATH!s and TUPLE!s simply have more dialecting power if they don't overlap what could be a legitimate user intention to fetch variables. This APPLY situation shows a good example of why inertness is good.)*

Also, if you want to literally pass a refinement value you can do so. Just quote it:

   apply :whatever [/refinement '/refinement]

That will pass /REFINEMENT as the refinement parameter.

Providing Normal Arguments By Name

A neat feature of new APPLY will be not only that you can reorder refinements to put them in any position, but also that you can name normal arguments.

So things like this would be legal:

>> apply :append [/dup 2, /value <d>, [a b c]]
== [a b c <d> <d>]

Everything is streamlined by the idea that refinements represent one (and only one) argument in the frame. Which has been a critical and elegance-making change.

The concept is that ordered parameters which have not yet been named would be filled in, in the order that they are supplied. You wouldn't be able to specify something twice, so this would be an error:

>> apply :append [/series [a b c], [d e f], <g>]
** Error: Duplicate argument supplied for SERIES

Can LOGIC! Be Used With Parameterless-Refinements?

I have belabored the point of why we don't want low-level FRAME! mechanics getting involved in transforming [#[true] #[false]] into [# null] for refinements that are "either used or not".

Those rules don't really "apply" here. APPLY is its own dialect with its own mechanics, and it can do what's convenient. It can canonize true and false into # and NULL in the frame.

foo: func [/usable /logicable [logic!]] [...]

apply :foo [/usable 1 < 2, /logicable 1 < 2]
; this would give usable of `#`, and logicable of `#[true]`

apply :foo [/usable 1 > 2, /logicable 1 > 2]
; this would give usable of NULL, and logicable of `#[false]`

Hopefully it makes sense why I think APPLY should be able to do this, while MAKE FRAME! and friends don't. MAKE FRAME! is the tool for writing APPLY-like-things, and there's no layer between the frame and the evaluator where it's appropriate to put these transformations. It should be kept pure.

Should You Be Able to Provide Refinements By Position?

I'm not sure if giving refinements by position is a great idea. But this hits on a philosophical question of whether refinements have an order that is exposed to the user.

They do have an order, because of the workings of modal arguments. If you make a parameter "modal" then it's the refinement immediately after them that gets set.

But specialization can shuffle the order. For instance, what was once a refinement can become a normal argument.

>> foo: func [x [integer!] /refine1 [integer!] /refine2 [integer!]] [...]

>> parameters of :foo
== [x /refine1 /refine2]  ; one normal argument

>> foo-2: :foo/refine2
== [x refine2 /refine1]  ; two normal arguments (e.g. acts like FOO/REFINE2)

This makes things a bit wacky.

I feel like the safest decision for right now is to say that once you've filled all the ordered arguments, any more arguments in the APPLY with no label causes an error. The place where this would cause issues would be if BLOCK!s are used to serialize and deserialize frame state generically. That's a thorny problem in general, which brings us to...

Should There Be An APPLY/ONLY

The idea behind APPLY/ONLY was that you have the values for a function's arguments already processed, and you don't want to evaluate them again.

One thing that breaks APPLY/ONLY in Ren-C's world is that this doesn't give a way to supply NULL arguments. One option would be to let the VOID! with label ~null~ be an escape mechanism for this, which might cover a lot of cases (though obviously not accurately capturing a frame that purposefully wanted to pass the VOID! value ~null~).

Because FRAME!s have come along as a general and pure tool for representing function applications, the pressure is taken off of APPLY to be all things to all people. I think it should start out as the handy evaluative form, and then wait for motivating scenarios before trying to bend to anything else. Maybe another operator entirely would be more suitable, or just using FRAME! as it is.

2 Likes