APPLY II: The Revenge!

...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!

Imagine if we let <- be an infix operator...taking the name of the function to apply on the left, and a block on the right:

<-: enfix func [
    'action [word! tuple! path! group!]
    args [block]
][
    apply (if group? action [do action] else [get action]) args
]

It's rather slick!

>> append <- [[a b c] <d> /dup 2]
== [a b c <d> <d>]

>> append/only <- [[a b c] [e f] /dup 2]
== [a b c [e f] [e f]]

Of course, you can mix it up with your own freaky ideas, even variadic ones!

>> $: 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]

The choice is up to you. Which is what all this is about!

2 Likes

A post was merged into an existing topic: Design Issues for New APPLY

Imagine if we let <- be an infix operator...taking the name of the function to apply on the left, and a block on the right:

I think it's worth trying.

I've been giving it a shot, and trying out other ideas too.

I tried colon, which looks pretty good in isolation:

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

>> append/only : [[a b c] [e f] /dup 2]
== [a b c [e f] [e f]]

But it kind of gets lost in the noise of all the other usages of colon:

thing: append : [series :value]

For most anything you'd put in that position, it struggles with the question of "why would it mean APPLY". Even if & wasn't hideous (and it is), there's no real tie-in that it has to other uses of & we might imagine...so having it do function application just looks random:

thing: append & [series :value]

What <- has going for it is that -> is the lambda function generator. So it looks like it's in the family of "function things". Pointing right creates a function, pointing left applies it.

>> (x -> [print [x + 20]]) <- [1000]
== 1020

So I think it's basically the best option. But...again...configurable for those who want to think differently!

1 Like

Mulling this has led me to a kind of seemingly inevitable conclusion...

I think this should be the behavior of terminal slash

Here it is demonstrated with one of rebmake's "that-sure-is-a-lot-of-refinements" functions:

add-project-flags/ [
    ext-objlib
    /I app-config.includes
    /D compose [
        (either ext.mode = <builtin> ["REB_API"] ["REB_EXT"])
        (spread app-config.definitions)
    ]
    /c app-config.cflags
    /O app-config.optimization
    /g app-config.debug
]

The slash marks a kind of barrier between the function and its block argument. It's like it's saying "whoa, hold up now, you're not actually taking that BLOCK! as a parameter!"

  • This avoids what's upsetting about a quoting apply add-project-flags [...] syntax. It may "look" nice on the surface, but you have to scan backwards to see "oh, there's an APPLY back there"

  • Non-quoting apply :add-project-flags [...] has the reverse problem: you think application is being suppressed and have to scan backwards to the APPLY and see "oh, no it's not, it's actually running".

Terminal slash kills two birds with one stone: it keeps things in the range of function application (e.g. APPLY/ONLY has a slash and that's connoted with application), and gives the visual separation we want at the location we want it.

This bakes it into the evaluator a bit more (though I'm proposing evaluator hooks that parallel UPARSE's combinator MAP! to permit varying it).

But the proof of concept that you could write a function doing it has been done. You can write this form of APPLY yourself with FRAME!, and design other APPLY-like operators. Just because you can write an apply operator doesn't mean that the evaluator has to avoid giving a foundational notation to this very critical behavior.

This will mean rethinking the other applications for terminal slash that are in the mix right now...

In The Evaluator, FOO/ Has Been A "This is a Function" Annotation

I had it so that if you terminated something with a slash it would run it...but warn you if it wasn't an action:

 >> print/ "Hello"
 Hello

 >> mystery: 10

 >> mystery/ "Hello"
 ** Error: MYSTERY is not an action!, can't run with terminal /

That's been mostly a means of commenting. A parallel concept I had was that you could ensure things where actions by assigning to paths with terminal slash:

>> x: 10

>> foo/: :print
== #[action! [line]]

>> bar/: x
** Error: Can't assign non-ACTION! using terminal slash

Terminal Slash Has Also Retriggered ACTION! Values

Today, this behavior is useful with GROUP!

 >> (either true [:print] [:elide])
 == #[action! [line]]

 >> (either true [:print] [:elide])/ "Hello"
 Hello

This becomes more useful with the ACTION!-vs-isotopic-ACTION! distinction. That's because plain WORD!s referring to actions wouldn't run in the evaluator. So foo/ could be a shorthand for run foo when FOO contained a plain inert action.

But...here's a thought: you probably really should be enclosing things in some kind of array anyway, if you're getting a function value somewhere:

 dump-block: func [block [block!] dumper [action!]] [
     for-each item block [
         print "Dumping item..."
         dumper item  ; what if dumper doesn't take exactly 1 argument?
         print "Item was dumped."
     ]
 ]

When reading some of BrianH's code when first looking at Rebol, I'd notice in cases like this the calls would be in GROUP!s, e.g. (dumper item). It would quarantine the function.. and sometimes it would take advantage of the fact that if the function took fewer parameters, that would be tolerated (saying it just wasn't interested in those).

But using dumper/ [item] instead of (dumper item) has advantages, such as that you won't be impacted by parameter conventions like quoting. So there's no way for the function you are calling to get the name of the variable you're passing--for instance.

In UPARSE, a FOO/ Has Been Running the ACTION! combinator

The implementation is a bit convoluted at the moment. But that it works at all as a usermode-implemented construct is pretty impressive:

  >> parse [1 2 3] [collect some [
       keep negate/ integer!
  ]]
   == [-1 -2 -3]

This is not necessarily contentious--given that you are free to kick over into a GROUP! if you want a function invocation that doesn't draw arguments out of the parse stream. But it could also be done by a RUN combinator that just picked up whatever came next.

  >> parse [1 2 3] [collect some [
       keep run negate integer!
  ]]
   == [-1 -2 -3]

And we could carry over the apply behavior in case you wanted a midstream application:

  >> parse [[a b] 2 "hello"] [
         series: <any>, n: integer!, append/ [series [c d] /dup n]]  text!
     ]
  == "hello"

  >> series
  == [a b [c d] [c d]]

At Time Of Writing, Terminal-Slash-APPLY Looks Like a Winner

I found that <- was no good for me, nor was //.

I think the other purposes terminal slash has been used for can be rethought, certainly for such a big payoff.

There'd still be the option of APPLY if you want it, but I'm now pretty firm that it shouldn't quote.

1 Like

I observe Red has published an implementation of APPLY with some seeming similarities:

Red Programming Language: Dynamic Refinements and Function Application

But there are significant mechanical and usability differences.

Red inherits complexity from the fact that they didn't merge refinements and their arguments together. This keeps them tied to problems that arise from ordering, and keeping the enablement of a refinement in sync with its value(s). It's clear to me that "multi-arg-refinements" has proven to be not worth it in the design vs. having a single unified nullable value for each named argument...and this is only one of the many places that bear that out.

They do offer an idea how to create something like a FRAME!... but it's a higher-level usermode concept instead of the lower-level basis for APPLY-like abstractions. The post gives an implementation called make-apply-obj-proto:

red>> o-fctm: make-apply-obj-proto/with 'find/case/tail/match [series: [a b c] value: 'a]
== make object! [
    series: [a b c]
    value: 'a
    part: false
    length: none
    only: false
    case: true
    same: fal...

red>> apply-object :find o-fctm
== [b c]

Ren-C bakes this idea in as the core of function application. You can build a FRAME! for the function (note the tail of the match is a multi-return...you can get both the begin and end in a single call...so there's no /TAIL here):

>> f: make frame! :find/case/match
== make frame! [
    series: ~
    pattern: ~
    part: ~
    skip: ~
    reverse: ~
    last: ~
]

Notice /CASE and /MATCH didn't take arguments, so they have been specialized out of the frame as they were already mentioned as being in use.

Then you can fill in the frame with required (and optional) values:

>> f.series: [a b c]
== [a b c]

>> f.pattern: 'a
== a

And it remembers what function it is for, so you can DO it without getting it mixed up. As a nice bonus, the multi-return gives you the head and the tail if you want it:

>> [begin end]: do f
== [a b c]

>> begin
== [a b c]

>> end
== [b c]

They say of make-apply-obj-proto: "But you may see that this is verbose and inefficient, making a whole object just for a call like this. And you'd be right. It's just an example. You don't want to recreate objects like this, especially in a loop. But you don't have to. You can reuse the object and just change the important values in it."

This is the opposite philosophy to Ren-C. These frames are the foundation of function invocation...and so they are always built. The keylist of the object lives with the function definition and is pointed to by the frame, and so each instance only takes up the cells of the arguments.

(Note: If you wanted something directly comparable to make-apply-obj-proto it really could just be an option passed to APPLY which gave back the frame it builds without calling it.)

Anyhow... I'm glad they've undertaken this... because it brings us closer to assembling comparable examples. It lays bare the fact that when I undertake "complexity" it is because failure to design the system to handle relevant cases pushes that complexity onto the user...

2 Likes