GROUP! for refinements in function invocation: worth it?

Back in the day, I thought it would be neat to allow you to put GROUP!s in paths. So you could do things like this:

append/(if condition ['only]) [a b c] [d e]

UPDATE circa 2024: refinements are now done with CHAIN!

append:(if condition ['only]) [a b c] [d e]

This particular GROUP! feature turned out to be of fairly limited use. Really you could only use it with refinements that didn't take parameters, because it changes the "shape" of the execution stream. Consider how you would make the following sensible:

append:(if condition ['dup]) [a b c] [d e] ???

When the condition is true you want something in the ??? spot. When it's false you don't. How can your code cover both cases?

As another thing, eventually I realized that failed IF has to break my heart and return NULL, not VOID. So in order to avoid missing out on the "soft failure" benefit of NULL values, you'd actually have to change this to one of:

append:(opt if condition ['only]) [a b c] [d e]

append:(? if condition ['only]) [a b c] [d e]

append:(when condition ['only]) [a b c] [d e]

Now we have a modern APPLY

apply append/ [[a b c] [d e] only: condition]

And you can use the // operator for brevity:

append // [[a b c] [d e] only: condition]

I'm In A Mood To Kill Off Lesser-Loved Features :hocho:

All things being equal, it might seem nice to support. But every feature has a cost!

Note you can EVAL some COMPOSE'd code if you really wanted to:

eval compose [
    append:(when condition ['only]) [a b c] [d e]
]

So if anyone has a good argument for keeping the function dispatch behavior, speak up now!

2 Likes

I wrote a short hack of a compatibility-APPLY for bootstrap, and changed all the GROUP! cases for refinements to use APPLY.

It's just better in pretty much all cases, e.g.

compile:files:(opt if inspect ['inspect]):settings compilables settings

=>

compile // [
    compilables
    files: okay  ; compilables represents a list of files
    inspect: inspect  ; return C source as text but don't compile it
    settings: settings
]

But...An Argument For Keeping It

When you think about dialects like UPARSE, they don't have an APPLY operator out of the box.

Being able to leverage the evaluative tool could be helpful, to someone who is running a GET on the PATH! and wanting a function back with the proper specialization. :-/

I'll let it stick around for now, for that reason. But let's try to avoid using it, and give APPLY some exercise.

2 Likes

Plus, A New Reason To Keep The Meaning Free...

If we can truly disconnect what parentheses mean in this context, we can also liberate brackets and fences, into something I have referred to as Dialected Function Calls.

This started from the idea of rewriting things like:

fail:blame ["This failure will implicate VAR at callsite"] $var

Into:

fail:$var ["This failure will implicate VAR at callsite"]

Basically, handing control over to the function to say "here's the CHAIN! that invoked you, why don't you interpret it how you want".

So if we assume CHAIN! doesn't presume the meaning of GROUP!... or BLOCK!... or FENCE!... or INTEGER! or $WORD! or anything... we get some nice expressivity:

For instance, what if COMPOSE could take the pattern you use to compose with?

>> compose:{} "some stuff {1 + 2}, (not composed)"
== "some stuff 3, (not composed)"

>> compose:[] "some stuff [1 + 2], (not composed)"
== "some stuff 3, (not composed)"

>> compose:((*)) "some stuff ((* 1 + 2)) (not composed) ((nor this))"
== "some stuff 3, (not composed) ((nor this))"

That example alone exceeds the usefulness of any cases I can think of where GROUP! evaluations were used to pick refinements.

Design TBD... :thinking:

I'm not sure exactly what API you'd use to specify that you want access to the CHAIN! to interpret yourself, or how you would delegate the chain processing to refinement processing so you didn't have to rewrite that bit (maybe WORD! is always interpreted as refinement, and your dialect can only heed things that aren't words?). And I don't know how it would work with APPLY.

But, it's something that seems fairly exciting to me if it could be pulled off... and I think I've talked through some of my binding concerns enough to where that wouldn't be the problem I thought it might.

I still think CHAIN!s that aren't function calls should treat GROUP! as they do today:

>> foo: null
== ~null~  ; anti

>> var: $foo
== foo

>> (var): 1020
== 1020

>> foo
== 1020

And probably a GROUP!-headed CHAIN! should still evaluate the first item as a GROUP! as it does today:

>> fn: append/

>> (fn/):dup [a b c] [d e] 2
== [a b c [d e] [d e]]

But the idea that there's a universe of dialecting available to the function in the CHAIN! has so much advantage notationally. It may be able to sort out the question of wanting to inject parameters at the beginning instead of the end when using refinements. (?)

1 Like

Outside of making CHAIN! dispatch more complex... this feature has another drawback, in that if it's supported we run into challenges with using refinements on infix functions.

If we know the thing on the right is a function call, and we can easily fetch the head of the chain to check the property of the function as infix or not...then there's no reason infix operators couldn't have refinements.

1 +:refinement 2

This doesn't mean you can't permit GROUP! at the head of a CHAIN, ignore such chains in lookahead, and just say "it errors if it looks up to an infix function after-the-fact". But isn't the feature kind of superfluous given the existence of modern APPLY?

apply (your expression) [args... refines ...]

(your expression) // [args... refines: ...]

Anyway, refinements on infix functions is something I've long wondered about, and it could be unblocked by dropping the GROUP!-at-head-of-CHAIN feature.

Although... TUPLE! needs to be legal sub-chain. And infix won't support tuples, and the TUPLE! might contain a GROUP!. :thinking:

Urgh. Well, things to think about. The main thing is that GROUP!s in the non-head position are more useful in dialected calls than they are in letting you pick the refinements.

I remembered another drawback.

If we don't say only WORD!-headed-CHAIN!s run functions, then we can't have a rule like "all other chains are inert". Which could be nice for dialecting, e.g.

>> n: 3

>> for 'x (n):5 [print [n]]
3
4
5

We wouldn't be doing the evaluation if you said it backwards:

>> n: 5

>> 3:(n)
== 3:(n)

So it would be a bit weird if we did it forwards.

The only qualm I have here about the "GROUP! in CHAIN! never evaluates" is that I've gotten kind of attached to:

>> foo: null

>> var: $foo

>> (var): 1020

>> foo
== 1020

Of course, the singular chains are different beasts. Trying to find some coherence here.

Consider also TUPLE!, which you might think always evaluates...but today if the head doesn't evaluate it doesn't evaluate GROUP!s in the rest:

>> 10.(10 + 10).(300 + 4)
== 10.(10 + 10).(300 + 4)

Should TUPLE! always evaluate any GROUP!s ?

>> 10.(10 + 10).(300 + 4)
== 10.20.304

You can quote it if you don't want evaluation. :man_shrugging:

I lean to saying that we may be better off saying TUPLE! always evaluates its elements, because PATH! does (or should) and it definitely has to evaluate its head. So we either get this:

>> (5 + 5).20
== 10.20

Or it decides that's just an error, because group-headed tuples are supposed to always imply picking, and the risk of the following is too great:

>> (expr).20
==> inert, because expr turned out to be inert

Regardless: I think it's best to kill off the idea that GROUP!-headed tuples dispatch functions. That clears up infix dispatch concerns along with a lot of other things. It's a little weird to make them inert, but it's a useful inertness.