I updated this thread a bit due to "Lift The Universe", which makes it possible to store voids in FRAME! using ^META fields. That helps the FRAME! protocol to speak in "as-is" values, such that you don't get a field forced into a lifted representation just because sometimes it needs to take unstable antiforms... you can deal with those cases more narrowly.
But one question about something like an <opt> parameter would be: should an ENCLOSE or ADAPT be seeing that parameter as a null, or as a void?
APPEND is a good example, because it has an <opt> on its value parameter.
>> append [a b c] () ; append needs to run...doesn't return null
== [a b c]
The VOID becoming a NULL is a convenience for the internals of APPEND, so that it receives a "stable" antiform of null instead of an unstable void.
To think about how this might work compositionally, let's take a real example... like writing the KEEP of COLLECT in terms of APPEND.
Here's sort-of how it used to be written, when it assumed that it got to see VOID...updated for lift the universe:
keeper: specialize ( ; SPECIALIZE to remove series argument
enclose append/ lambda [ ; gets :LINE, :DUP
f [frame!]
<with> out
][
if void? f.^value [ ; imagine it sees VOID, not NULL
return null ; doesn't "count" as collected
]
f.series: out: default [make block! 16] ; won't return null now
eval f
f.value ; KEEP returns the input value as output
]
)[
series: <replaced>
]
This raises several questions, beyond just the question of voids and nulls for <opt>.
KEEP twists APPEND such that it returns the value you give it... should it be getting decayed inputs, or should it have access to the undecayed forms? e.g. could it say:
eval f
^f.value
If it got the inputs decayed and received PACK! for instance, that would allow:
>> collect [probe keep [10 20]]
\~['10 20]~\ ; antiform
== [10]
But APPEND doesn't handle PACK!... its value argument is non-^META. Does that mean the decays have happened before the ENCLOSE has been reached? Does it mean other typechecking has been done before the ENCLOSE?
So far, the assumption has been that if you ENCLOSE a function, you're subject to its interface...the type checking is done before your enclosing function runs. So you'd have to adjust the interface before doing an enclose if you wanted to widen it to accept more types or change the parameterization. However, that means any parameters you change have to be typechecked again.
This runs up against ideas I've had about typechecking and decay being something that happens "inside" the function dispatcher. If APPEND hasn't been invoked yet, then something has to get involved and process the frame so that the ENCLOSE gets the FRAME! as APPEND would see it.
But you face questions there of whether conventions like <opt-out> fit in with that, too. Does an ENCLOSE of a function that has an opt out parameter get a chance to not opt out... e.g. does the opting out happen on the evaluation of the frame, or does it happen in the frame processing?
Practically speaking, it seems to me most ADAPT and ENCLOSE calls want type checking to have been run. At least, most do today.
What bearing does this have on building a FRAME! manually? You still have type checking run on it before execution. But type checking has to run on frames that you tweak during an ENCLOSE or ADAPT as well.
This implies that the <opt> transformations that turn voids into nulls are not part of type checking, because you don't want them to run every time you EVAL a frame. They're part of the parameter convention, so like quoting.