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.

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 BAD-WORD! 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 ~null~ BAD-WORD!).

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

I was debugging something in the not-really-used-but-still-there encapping code, and saw one of the many kind of lousy line-breaking situations:

    section-headers-data:
        read/seek/part file e_shoff (e_shnum * e_shentsize)

The code is indented enough that it would exceed the desired 80-column-limit to put that all on one line.

What I sometimes do with this to try to communicate is put the whole thing in a GROUP!:

    (section-headers-data:
        read/seek/part file e_shoff (e_shnum * e_shentsize)
    )

But it's crappy enough that I avoid doing it, as cases like this one show.

With the proposed design for APPLY, you'd have an alternative...with comma and line break options too:

    section-headers-data: apply :read [
        file /seek e_shoff /part (e_shnum * e_shentsize)
    ]

    section-headers-data: apply :read [
        file, /seek e_shoff, /part (e_shnum * e_shentsize)
    ]

    section-headers-data: apply :read [
        file
        /seek e_shoff
        /part e_shnum * e_shentsize
    ]

In the post above, I question whether apply read or apply :read is better. But maybe there's an even better angle if you want more readability.

It's A Common Desire...Should It Have a Foundational Syntax?

I want to take terminal dot to mean "this is not a function call", e.g. var. would error if var was a function. So terminal slash was the complement...to mean "this IS a function call".

But what if terminal slash meant "this is a function call... and it's also an apply form"

    section-headers-data: read/ [
        file
        /seek e_shoff
        /part e_shnum * e_shentsize
    ]

This wouldn't have to lose my concept that :var/ would be "get me the variable and ensure it's an action" meaning... though it would lose the plain syntax of foo/ x y z of "be sure this is a function and run it", you'd have to say foo/ [x y z] instead. Though when you think about it... if someone is obsessive enough to need assurance that foo is a function, might they also be obsessive enough to want to specify a precise number of arguments to it?

Seems Promising...

Anyway, an interesting thought. It could put a close to the "should APPLY quote" debate, and say it wouldn't, because you'd only use APPLY with functions coming from expressions if you had this form.

...and by *"it's time" I apparently meant "within the next year, maybe"...

But better late than never, right? It's in!

Refinements Can be Provided In Any Order

[a b c d e d e] = apply :append [[a b c] [d e] /dup 2]
[a b c d e d e] = apply :append [/dup 2 [a b c] [d e]]
[a b c d e d e] = apply :append [[a b c] /dup 2 [d e]]

[a b c d d] = apply :append [/dup 2 [a b c] [d e] /part 1]
[a b c d d] = apply :append [[a b c] [d e] /part 1 /dup 2]

Any Parameter (Not Just Refinements) Can Be Used By Name

Once a parameter has been supplied by name, it is no longer considered for consuming positionally.

[a b c d e] = apply :append [/series [a b c] /value [d e]]
[a b c d e] = apply :append [/value [d e] /series [a b c]]

[a b c d e] = apply :append [/series [a b c] [d e]]
[a b c d e] = apply :append [/value [d e] [a b c]]

Commas Are Ok So Long As They Are Interstitial

[a b c d e d e] = apply :append [[a b c], [d e], /dup 2]
[a b c d e d e] = apply :append [/dup 2, [a b c] [d e]]

>> apply :append [/dup, 2 [a b c] [d e]]
** Script Error: end was reached while trying to set /dup

Giving Too Many Arguments Is An Error

>> apply :append [[a b c] [d e] [f g]]
** Script Error: Too many values in processed argument block of APPLY.

Refinements Must Be Followed By A Non-Refinement

>> apply :append [/dup /part 1 [a b c] [d e]]
** Script Error: end was reached while trying to set /dup

But you can pass refinements as arguments to refinements...just use a quote!

>> tester: func [/refine [any-value!]] [refine]

>> apply :tester [/refine '/ta-da!]
== /ta-da!

No-Arg Refinements Permit LOGIC! But Set NULL or #

Remember: the DO FRAME! mechanics do not change anything, besides ~unset~ isotopes being turned to NULLs. So if a refinement doesn't take an argument, the only legal values for that refinement in the frame are # and NULL.

But APPLY isn't DO FRAME!. It's a higher level thing that builds a frame from the values you supply, and then has an opportunity to look over them before running DO FRAME!. So if it sees you gave a #[true] or a #[false] to a refinement with no argument, it will adjust it appropriately.

>> testme: func [/refine] [refine]

# = apply :testme [/refine #]
null = apply :testme [/refine null]

# = apply :testme [/refine true]
null = apply :testme [/refine false]

^META Arguments Are Also Accounted For

For those following the profound design points, the DO FRAME! mechanic does not allow you to have isotopes in frame slots. The way you get isotopes through to a function is through meta parameters, and by convention those parameters are quoted or otherwise "leveled up" into non-isotope status.

But as another convenience, APPLY detects when a parameter is meta and will level it up...because the low-level frame mechanics aren't allowed to editorialize:

>> non-detector: func [arg] [arg]  ; not a meta argument, isotopes illegal

>> apply :non-detector [~baddie~]
** Script Error: non-detector needs arg as ^META for ~baddie~ isotope

>> detector: func [^arg] [arg]

>> apply :detector [~baddie~]
== ~baddie~

I know not everyone has gotten their heads around isotopes yet, but they are critical... this stuff was the missing link to making it all gel.

:dizzy: :dizzy: :dizzy:

What's Next?! Making It Easier To Use!

Let me just offer a quick example showing that our hands may not be as tied as we think...

Let's make $ a function that takes the name of the function to apply on the left and variadically takes args from the right:

>> $: enfixed func ['name [word!] 'args [<variadic> <end> any-value!]] [
       args: make block! args
       apply :(get name) args
   ]

>> data: [a b c]

>> (append $ /dup 2, data [d e]) print ["data is" mold data]
data is [a b c d e d e]

While it may not be perfect, it certainly shows that there are options. We could have done it as:

($ append /dup 2, data [d e])

Just a line of thinking to consider. But certainly, to consider how the evaluator bends...

2 Likes

On this note, it seems Red's APPLY is (maybe?) going for the case of "worldview 1", where you can say apply append [...] instead of apply :append [...]

apply.red · master · hi i am boris / Red mezz warehouse · GitLab

Boris says:

"Also, should function name be a lit-arg?
I expect the main use cases will have a literally written function name,
and where that is not enough, a paren could be used. E.g.:

apply (pick [func1 func2] condition) [...]

Lit-arg will also allow us to pass a function value to it without parens (though currently that's impossible since we can't use function value to construct a path)

apply :myfunc [...]

As a Reminder of My Opinion... "Worldview 1.5"

I guess maybe Red's usage gives the opportunity for them to be the guinea pigs. If it bites people then they can be the one to find out.

Gives me Thought: Might "APPLIQUE" Be Unified?

They're making it so you can use an object to apply parameters. If you have an already built object that can be used for a function in Ren-C, that has a name... it's called FRAME!, and you can just DO it. You don't need to APPLY it because it knows the name of the function it was created for.

So APPLIQUE is a version of APPLY that runs code bound into a frame.

>> flag: true

>> applique :append [
       series: [a b c]
       if flag [
           value: <control>
       ] else [
           value: <structures>
       ]
   ]
== [a b c <control>]

But APPLIQUE is a (deliberately) crappy name. I didn't want to take something that would be used for something else.

Should this be folded in as a variant of APPLY of some kind? APPLY/FRAME? APPLY/CODE? Or a different datatype?

>> apply :append [...]  ; act like normal APPLY

>> apply :append @[...]  ; act like APPLIQUE

In some sense what you want to say here is actually kind of like:

>> do make :append [
       series: [a b c]
       if flag [
           value: <control>
       ] else [
           value: <structures>
       ]
   ]
== [a b c <control>]

That would break it out into two steps; one step where you build the frame out of code, a bit like a MAKE OBJECT!.

But the problem here is that you're passing make an ACTION!, and getting a FRAME! back. :-/

Right now when you make a frame you say make frame! :append and all your parameters are taken. So the only way out of this looks like to allow MAKE taking an ACTION! instance in for "what to make" and giving you a FRAME! back. Which is weird.

Anyway, just mentioning because Red is using APPLY to mean two different things, though their division doesn't make sense in Ren-C's (quite superior :slight_smile:) model.

1 Like

While that might kind of suck, it just was to get the point that there are options. And I am quite convinced...

We Will Need An Apply Operator, We Just Have To Decide On It

I think the floodgates are pretty much open on "weird words" now. The fact that you can't make them into SET-WORD!s or put them in PATH!s will just be life. Maybe they'll be their own category distinct from WORD!, like SYMBOL! ?

Among things we'll get sooner or later are : and ::

append :: [data [d e] /dup 2]

That's good-looking, but maybe too good looking...I think it should be saved for something in the type system as in other languages.

Arrows are associated with functions, and since -> was being used for lambdas, I said that using leftward arrow for something "function related" made more sense than other things. Well... APPLY is function-related, is it not?

append <- [data [d e] /dup 2]

Maybe POINTFREE could be shorthanded with lambda pointing at a GROUP! instead of a BLOCK!, or something along those lines?

>> ap10: -> (append _ 10)

>> ap10 [a b c]
== [a b c 10]

>> mp2: -> (multiply 2)

>> mp2 10
== 20

It seems to me that if we're concerned that someone wants to actually calculate a body to use for a function, they wouldn't be using the shorthand lambda notation to do that and would use full FUNC. Or those oddballs would be all right using -> :(...) if they wanted that.

There's only so many keys on the keyboard in this universe. How about it, <- for APPLY?

2 Likes

I think it's worth trying.