Should You Un-Ask For Refinements w/OPT, not NULL?

We don't want to make things ugly for the sake of mechanical consistency...

...BUT.... some levels of coherence are overwhelming and shouldn't be fought against.

Another piece of the puzzle has clicked into place with the return of "HOLES"... what had once been antiform PARAMETER! in function frames, to indicate a parameter should be gathered.

Antiforms were frustratingly "not-quite-out-of-band-enough"; they were values that had to be considered by a function like INTEGER?. And so if you did a MAKE FRAME! for the INTEGER? function, there was a question of semantics of what it meant to have an antiform PARAMETER! in the value slot:

  • does the antiform parameter mean "this function is unspecialized?" (e.g. INTEGER? still needs an argument to test)

  • or does it mean "this function is specialized with an antiform parameter" (e.g. INTEGER? should return null, because that's not an integer)

But with bedrock PARAMETER! (a "dual state"), we get a hybrid solution. Something that cares (like function dispatch machinery) can find out that it's actually a hole--hence an unspecialized parameter. Things that don't care (like DEFAULT) can experience the value as if it's a GHOST!.

Hence MAKE FRAME! by default looks like a bunch of ghosts, to a casual observer.

This makes it feel all the more natural to say that an unused refinement was just never filled in.

(You might think there could even be a feature hiding in this: if we left the unused refinement cells as holes, then they'd know the types of the values they would have taken--while still reacting as GHOST! to most observers. But I don't think that's a great idea, because you couldn't easily keep that guarantee if someone wrote frame.refinement: if 1 = 2 [<value>] and overwrote the slot with void...that would throw away the information, and I don't think we'd want to be in the business of putting it back. So canonizing the refinements to "actual ghosts" seems the better plan.)

The 0-Argument Refinement Finesse

The unused 0-Arg refinement value has always been a pain point. Not just in Ren-C but in historical Rebol.

Rebol2 said refinements were either true, or none:

rebol2>> foo: func [/refine] [probe refine]

rebol2>> foo/refine
true

rebol2>> foo
none

Red says they are either true or false:

red>> foo: func [/refine] [probe refine]

red>> foo/refine
true

red>> foo
false

Two things are putting pressure on here:

  • the universality of an untouched function parameter being in the "unperturbed" state ("all unused refinements are the same")

  • the greater usefulness of a 0-arg refinement being a LOGIC!

Ren-C offers a true "out"... because :REFINE will turn ghosts into NULL. This means the frame cell can be a GHOST! and yet you can experience it as a logic (or as a ghost, if you do ^REFINE). So long as the used state is an ~OKAY~ antiform, this works!

:clap:

Wrinkle: COND vs. OPT

Generally speaking, COND (ghost/empty-pack) is used to "opt out", while OPT (none/empty-slice) is used to "opt in".

But when you use COND on a refinement, you're not opting out of the function call as a whole. You're just not asking for the refinement. The function still runs.

Hence a refinement has parity with a normal argument that takes GHOST!, and receives ghost when a ghost is passed.

This isn't a mechanical problem... but it's weird:

>> append [a b c] if 1 = 2 ['d]
== \~null~\   ; antiform

>> append:dup [a b c] 'd if 1 = 2 [10]
== [a b c d]

The only inkling I have of what it might imply is that the OPT/COND distinction is too subtle and weird, and maybe in the era of "hot potatoes" what COND makes should be a ~(veto)~... and these functions returning null are thus veto-reactive.

This makes more sense, even in the function spec. If you see <veto> in a function argument, that means if that argument is a ~(veto)~ then the function will gracefully return null.

(Note: you can't assume this systemically for any argument, e.g. VETO? has to accept a veto and return true. And you wouldn't want (integer? veto) to return false, it needs to error. I'm not sure if there's some general rule to when veto-ing is okay; you don't want something that is making a decision that could be false meaningfully to be vetoable, so anything that can return a LOGIC! naturally can't be veto'd...)

Wrinkle: 0-Arg-Refinement NULLs

If a refinement takes 0-arguments, it's awfully tempting to set that with a logic.

>> f: make frame! append/

>> f.series: [a b c]
>> f.value: [d e]

>> f.line: integer? f.value

Instinctively, I feel like the typechecking machinery has to be willing to canonize a null passed to a 0-arg refinement into a ghost.

The only reason I'd say it shouldn't do that would be if it can't. I've tried to talk about these "moments" of frame transition, and that's the only real worry... is when there isn't a "moment" to do it... or if the multiple moments have competing needs.

What's good about the canonization to the ghost is that it suggests all the moments have the same endpoint. It's not like some phases want an endpoint of null, and some phases want an endpoint of ghost... they all want ghost. So it isn't canonization that's the problem, it's different phases wanting different canonization.