Should refinement arguments be REFINEMENT! if used?

In Rebol2, a refinement argument would be #[true] or #[none]:

rebol2> foo: func [/bar] [print mold bar]
rebol2> foo/bar
true ;-- actually LOGIC! #[true], not the WORD! true
rebol2> foo
none ;-- actually a NONE! value, not the WORD! none

One thing that annoyed me about it was that you couldn't check refinements with AND, OR, etc... since NONE! couldn't be arithmetically AND-ed with things. So I thought it should always be LOGIC!, as #[true] or #[false], and that had a feeling of consistency.

Then it came on the table that AND and OR would be conditional. In the meantime, I joined the camp of saying it was okay to just use ANY or ALL... so the idea of the not-present state being NONE! (blank! in Ren-C) didn't seem so bad.

But that made it seem like it would be cool if the truthy value had some particular use.

Early Ren-C experiment: use WORD! or BLANK!

Rebol2 couldn't accept refinements by GET-WORD! or in a GROUP! calculation (you couldn't say append/(second [dup only]), for example). But Ren-C added that. This meant for chaining, it could be leveraged if used refinement values were a WORD! name of the refinement itself:

oldrenc> foo: func [/bar] [print mold bar]
oldrenc> foo/bar
oldrenc> foo

If one function took /only and called another function that also took an /only, then your refinement could be called only, and you could just say chained-function/(only) and that would either resolve to chained-function/(_) or chained-function/('only)... so it would "just work". That depended on BLANK! just being ignored when passed to a function.

Some people were a bit uncomfortable at the "Inception-like" idea that the word "only" would look up to "only". Today I'd suggest it's probably better to have the variable look up to a REFINEMENT!, hence being /only. That would help make what's going on a little clearer I think.

But one thing I noticed quickly is that while it was useful, it wasn't as useful as it might at first seem. Most of the time, it only comes up when a refinement has no arguments. Because if a refinement does have arguments, you'd have to not just opt out of it...but also opt out of its arguments even being there!

append/(if condition [/dup]) block data ??
print "arity gets changed based on refinement with arg use, bug!"

You'd end up having to change the structure of the code itself at a higher level, with COMPOSE or similar. Ren-C had the mechanism of disallowing NULL as a refinement, so this was done with always supplying the refinement and then having NULL used to revoke it, like this:

append/dup block data if condition [...]

Also, as specializations/adaptations/etc. became the main way of building compositions it became less critical...and less common for you to want to take a refinement from one function and call another with it. The naming overlap started seeming somewhat coincidental, if the functions weren't somehow derived from each other.

But given that it was still demonstrably useful for refinements with no arguments, something had to make it seem like a bad idea to get cut. What got me to cut the feature--back then--was concern over what it did to the variability and typechecking of frames:

oldrenc> foo: func [/bar] [print mold bar]
oldrenc> f: make frame! :foo
oldrenc> f/bar: true
oldrenc> do f ;-- run once
oldrenc> do f ;-- run twice
** Error: bar must be LOGIC! and not WORD!

The evaluator seemed to be editorializing. It was changing false to blank, and true to refinements. Of course, it could be made to accept refinements instead of logic, and blanks instead of false. But I didn't like the idea of it taking any "truthy" thing. Should you be able to say only: 'dup and have it resolve that to only? Forcing you to supply LOGIC! made it seem more like you knew what you were doing.

It made me feel uncomfortable, because I had the idea that the evaluator would be treating frames as read-only so they could be used multiple times. Yet this required mutating the frame to conform it. I decided to back it out and focus on the function compositions.

Once again, the times have changed...!

Here's some new knowledge to inform this decision:

  • FRAME!s do not get reused. Every time you DO a FRAME!, the evaluator takes ownership. Callers are expected to COPY frames before calling DO if they plan to use it again.
  • It's already the case that the evaluator has to modify refinement cells. If you MAKE FRAME! :SOME-FUNC, all the cells in that frame are initially empty. However, you don't have to go through and set every refinement to FALSE before you DO the frame! It sets them to false for you, so you don't wind up with unset it's changing stuff as it is.
  • A hard rule has emerged that you can't use NULL in PATH!s. So append/() is not legal, nor is append/(if false []). You need a BLANK! to opt out of a path step, e.g. a refinement to a function--which means putting together conditionals to opt out always needs to look like append/(try ...expression...).
  • We now have the conditional AND, OR, XOR that were only theorized before...and they're better than anyone might have thought they would be.
  • Being overly protective about the input types for refinements on APPLY seems like a somewhat inconsequential detail. If you're forced to say some-refine: did expr how much did that really gain the system? Every IF statement has an "implied TO LOGIC!" on its condition, and now we have protections against voids and nulls, so that seems like enough.

All factors weighed, I'm thinking this should go in

I've missed this feature, and the fact that you can't opt out of PATH! slots with null have made it seem all the more necessary.

The frame-based technical points that concerned me before don't really exist anymore. The specialization mechanics are such that once it becomes a REFINEMENT! and gets flagged as type checked, it won't be checked again. So even concerns over what it might do for performance--checking the spelling each time etc, aren't an issue.

If we switch it to REFINEMENT!, that seems like it would reduce people's confusion.

All told, I think it's a win for chaining. It might even speed up the system. Do people have any comments/questions/reservations?

1 Like

I should add that one area I found started to benefit that I hadn't thought about was debugging. Look at this test:

UPDATE a year later: With the refinements as their own arguments change, the following is different; a refinement with an argument would not have a form where it is named in this fashion (nor would it need a name to be chained)

foo: func [/A aa /B bb /C cc] [
    return compose [
        (if A [/A]) (:aa)
        (if B [/B]) (:bb)
        (if C [/C]) (:cc)

fooBC: :foo/B/C
fooCB: :foo/C/B

did all [
    [/B 10 /C 20] = fooBC 10 20
    [/A 30 /B 10 /C 20] = fooBC/A 10 20 30

    [/B 20 /C 10] = fooCB 10 20
    [/A 30 /B 20 /C 10] = fooCB/A 10 20 30

    error? trap [fooBC/B 1 2 3 4 5 6]
    error? trap [fooBC/C 1 2 3 4 5 6]
    error? trap [fooCB/B 1 2 3 4 5 6]
    error? trap [fooCB/C 1 2 3 4 5 6]

Firstly, that is a cool test, look at it. :slight_smile:

But that suggests that if an in-use refinement is already the REFINEMENT! itself, that has applications in the display arena. You wouldn't need if A [/A], you'd just say A. (Obviously more of an advantage the longer the refinement name.)

And I've mentioned how foo/(if only [/only]) won't work, you have to say foo/(try if only [/only]). I think it falls under more-good-than-harm to be able to say foo/:only or foo/(only).

1 Like

This feature is back in the news, because with the de-stigmatization of NULL access I'm feeling like having the NULL vaporize here is probably good. Hence no TRY needed.

Before, I was afraid of what this might mean for typos, like foo/(:some-varable). If NULL was silently accepted, this seemed dangerous. Now that typo situation would be a VOID!.

Not all types have to accept this as vaporization. What this means is that in the PATH!-processing logic, it's now up to the type getting the argument as to whether a NULL is tolerated. ACTION! paths can say it's all right, MAP! or OBJECT! access can error. (Previously it was illegal at a path dispatch level, so no data type could accept it.)

For compatibility we can accept BLANK! here as well too for now.

To rephrase the title question in the current environment:

Should refinements taking no arguments be REFINEMENT! if used?

First off, I definitely like the concept of NULL if unused, better than false. Because a LOGIC!-refinement in the new framing is effectively a tristate:

foo: func [/mutate [logic!]] [...]

This refinement can be NULL (unspecified), or #[true] or #[false] when specified. So I think for refinements being their own arguments...then a LOGIC! false state would be implicitly misleading. It needs a NULL and then a single truthy state.

It's hard not to like the ability to chain. So some word form of the refinement carrying its name should be the value. Since REFINEMENT! no longer exists (it's a PATH!), there's a likely cost to using this form instead of something like @ref-name, where being a non-reducible form has the advantage of being able to compose nicely.

So I think the jury is still out on whether this should be a PATH! or not. But at least I think a couple of other points are settled into place.