Default Values And MAKE FRAME!

Something you may notice when you MAKE FRAME! is that all the fields start out VOID (hence we say the variables are "unset"):

; Remember that the QUASI! of NULL would be QUOTED!
; e.g. [series: '~] --  But since these tildes are not quoted they imply
; evaluation, which would set the variables to void (e.g.)

>> f: make frame! :append
== make frame! [
    series: ~
    value: ~
    part: ~
    only: ~
    dup: ~
    line: ~
]

It's Critical These Are Isotopes And Not NULL

The most important reason is that when a function is invoked, its frame cannot actually contain any isotopes. So they can be out-of-band, and safely represent the intent of an unspecialized parameter.

Isotopes can only be passed as arguments is if they are ^META parameters...and this means their value in the frame will have been pre-processed into a plain BAD-WORD!.

Why is it plain? Well, remember the META behaviors:

>> meta null  ; META of pure NULL gives BLANK!
_

>> ~whatever~  ; evaluation of a plain QUASI! WORD! produces an isotope
== ~whatever~  ; isotope

>> meta ~whatever~  ; META of an isotope gives you the QUASI! WORD! back
== ~whatever~

>> meta 1020  ; META of any other value will just quote it one more level
== '1020

>> meta the '''(a + b)
== ''''(a + b)

So the evaluator is armed with the knowledge that the frame cannot legitimately have isotopes to be passed as arguments. It can draw the conclusion that if it does see ~ isotopes in a frame, that means the parameter was not specified.

For refinements it's fine if the parameter was unspecified; they can just be defaulted to NULL. For a required parameter, it means raising an error...just as if you had the parameter missing at a callsite.

You Can Specialize-Out Optional Arguments With NULL

Another perk of starting out the frame with voids is that you can remove parameters while leaving other parameters unspecified. This permits partial specialization:

>> foo: func [x [integer!] /y [integer!]] [if y [x + y] else [x + 1000]]

>> f: make frame! :foo
== make frame! [
    x: ~
    y: ~
]

>> f.y: null

>> bar: make action! f

>> parameters of :foo
== [x /y]

>> parameters of :bar
== [x]

>> bar 20
== 1020

This shows pretty clearly why we don't want NULL to denote unspecialized fields--because it's desirable to be able to specialize them to NULL! The distinction from void lets the evaluator notice when that's happened.

You're Guarded By Errors When Using Unset Fields

While you're filling up the frame, it's nice to have a heads-up if you access frame fields that haven't been assigned yet:

>> if f.series [print "An error here makes sense, right?"]
** Script Error: f.series is unset (~ isotope) (see ^(...) and GET/ANY)

The Locals Are Initialized to the Unset Isotope, Too

This is something a bit hidden, because you don't see the locals when you are viewing the frame from the "outside" (as you are here).

But from an implementation standpoint, it's nice to have all the locals start out as unset, which is what you want them to be when the function runs.

1 Like

That Seems So Elegant... What's The Catch?

  • When doing a low-level build of a FRAME! for a function, you are on the hook for knowing the callsite parameter convention and doing what it takes to meet the expectation.

    • So for a quoted (literal) parameter, you have to take that into account...since no callsite quoting is going on, you must do your own quoting in the assignment.

      • This isn't particularly new--you had to do your own quoting when using Rebol2 APPLY, also. The quoting convention in the function spec wouldn't be heeded inside the apply block.
    • Same for a meta parameter...there's no callsite that's converitng things from isotopes into a non-isotope, or adding quote levels. When you are assigning fields in the frame you have to remember to META them.

It means that if the parameter convention changes, what you might have written for a MAKE FRAME! previously won't work.

Let's say someone writes a function that returns if something is greater than 10:

greater-than-10: func [x [integer!]] [
    return x > 10
]

Then you write code that builds a frame for it:

f: make frame! :greater-than-10
f.x: 20
assert [true = do f]

It works. Yet later the person who wrote the function decides they want to do something special if it's passed an isotope, while keeping the behavior for integers the same:

greater-than-10: func [^x [integer!]] [
    if quasi? x [
        print ["Doing special thing for" mold x]
    ] else [
        x > 10
    ]
]

>> greater-than-10 20
== #[true]

>> greater-than-10 ~asdf~
Doing special thing for ~asdf~

Why did the person who switched the parameter to ^META add this feature? Who knows. But let's say they thought it was okay because existing callsites would remain the same.

But it breaks our invocation via FRAME!...the code won't work any more.

>> f: make frame! :greater-than-10
>> f.x: 20
== 20

>> do f
** Error: ^META arguments must be quoted!, bad-word!, or null

That message should be better, and tell you that it's x that's the problem. But the key is you have to now adjust how you fill the frame to meet the meta requirements:

>> f: make frame! :greater-than-10
>> f.x: quote 20
== '20

>> do f
== #[true]
2 Likes

Higher-Level Functions than DO FRAME! Can Lend A Hand

Let's imagine you want to specialize RETURN. It's one of those functions that takes a ^meta parameter, so that you can say things like return ~none~ and hand back an isotope to the caller.

But specialize can be creative in terms of how it builds the frame. So let's say you don't want to care if it's a meta parameter or not, and want to write:

r5: specialize :return [value: 5]  ; you don't want to have to quote 5
rU: specialize :return [value: ~]  ; e.g. return void

So imagine specialize isn't using voids as its initial condition for variables in the frame.

Just for starters, let's have it use some kind of one-off series identity for the initial state of value. A primordial example could just use a string like "!!!unspecialized!!!". If that were owned by SPECIALIZE then that could uniquely identify unspecialized fields...and assigning an identical string with a different identity wouldn't be mistaken for the identity that specialize knows.

So then, anything it saw that it didn't recognize as that exact identity, it would assume had been specialized. And if the parameter class was ^META, it would do the appropriate adjustment...turning isotopes into QUASI! WORD!s, and quoting other values.

What you lose in this "specialized identity" approach being a string is the idea of getting errors on unassigned variables...or having an easy test of whether you've specialized things or not. This might be a good argument for having something that carries a cheap notion of identity but can be recognized as unspecialized. Maybe UNSPECIALIZED! should be its own datatype for the purpose?

But the point is... higher-level tools can come into play that are aware of the parameter conventions and do adjustments for you. But if you work at the FRAME! level, you're at the metal...and you have to fill the slots with their final values; no parameter conventions will be applied to adjust them for you.

The only adjustment that happens in frame execution is that voids will be converted to NULLs.

And I will say again that as weird as it all may sound, it's quite elegant!

1 Like

So now, this works!

unset: redescribe [
    {Clear the value of a word to an unset isotope (in its current context)}
](
    specialize :set [value: ~]  ; tricky case, but supported!
)

It's been a long path to this but I think it's a good one. We realize that what a higher-level construct like SPECIALIZE or APPLY can do is different from the raw FRAME! mechanic.

There will be some people out there who use FRAME! and then are bitten by the fact that a function they think is "ordinary" (like SET or RETURN) turn out to take ^meta arguments. But it just means you have to match the parameter convention.

As I've said before, this is true of quoting as well. Even if you're used to writing for-each x ... you can't do:

 for-each-x-frame: make frame! :for-each
 for-each-x-frame.vars: x

The quoting from the parameter convention doesn't apply to the assignment inside the frame. You have to say:

 for-each-x-frame: make frame! :for-each
 for-each-x-frame.vars: 'x

And similarly for a meta parameter. You need the ^.

 return-unset-frame: make frame! :return
 return-unset-frame.value: ^ void

Makes pretty good sense, and here SPECIALIZE is showing that higher levels can smooth over it.

2 Likes

Okay... in practice this catch turns out to be really annoying.

Enough so that I think the frames should speak an "as-is" language. Then the mechanics that turn parameters into ^META should be done by the function when it's called.

So let's revisit:

greater-than-10: func [^x [integer!]] [
    if quasi? x [
        print ["Doing special thing for" mold x]
    ] else [
        x > 10
    ]
]

I think what should happen in practice is that FUNC turns that into something like this, where you can annotate a lower-level FUNC* to say that isotopes are okay...and the body ^METAs the variables:

greater-than-10: func* [x [integer! ~any-value~]] [
    x: ^x  ; This would be added by the higher level FUNC generator

    if quasi? x [
        print ["Doing special thing for" mold x]
    ] else [
        x > 10
    ]
]

Here you'd get the behavior you'd expect for normal values:

>> f: make frame! :greater-than-10
>> f.x: 20
== 20

>> do f
== #[true]

And if you wanted to pass an isotope, you'd assign isotopic values in the frame:

>> f: make frame! :greater-than-10
>> [~f.x~]: ~asdf~
== ~asdf~  ; isotope

>> do f
Doing special thing for ~asdf~

But What If Actually Want To Specialize To Void?

You wouldn't be able to specialize voids by way of just a FRAME!.

>> f: make frame! :set

>> f.value: ~
; void  (...meaningless, as it was already void...)

>> unset: make action! :f  ; not going to give you what you want

>> unset 'foo
; UNSET is missing its value argument

I'm not all that bummed out about this. It was much more of a problem when you couldn't set things to NULL, because that inhibited removing refinements from the interface of functions. This is a really narrow case...and I think some operation that just says "remove the parameter from the interface would probably do it.

>> f: make frame! :set
>> protect/hide 'f.value  ; commit to the unset state
>> unset: make action! f

And SPECIALIZE could still do it, using the trick of assigning a special identity to each frame variable that it recognizes:

unset: redescribe [
    {Clear the value of a word to an unset isotope (in its current context)}
](
    specialize :set [value: ~]  ; tricky case, but supported!
)

What About Signaling END of Frame Input?

I mentioned that the end of input could be NULL if the ^META parameters were always QUASI! / QUOTED! / BLANK!.

But at the frame currency level, this doesn't work if it's using plain values and not meta ones. You have conflation of void, and then if you use NULL you're getting conflation with END.

Something that could solve these cases would be if there were "meta frames" where every value was interpreted as being meta. Could this be an application for isotopic frames??

Or a behavior for QUOTED! frames?

>> f: make frame! :set
>> f.value: '~   ; meta void
>> make action! isotopic f  ; isotope status means unquote everything

I don't know about that, because I was thinking isotopic frames would auto-run from WORD!, and save you from having to make ACTION!s out of frames just to get that behavior.

Anyway, long story short, I think the baseline mechanics of FRAME! has to be as-is. Whatever else is an exception and should be handled specially. If you want a ^META parameter, that should be something that happens after the function starts running. This improves matters greatly.

As is often the case... the old way had right parts, the new way had right parts, and the answer is going to need... nuance. :roll_eyes:

The way it works today, frames are initialized to "nihil" (the isotope of void), because ~ is what denotes the unset state. For I-believe-to-be-good-reasons, it's no longer a synonym for void itself (the meta/quoted form of void is a single ' apostrophe):

>> f: make frame! :append
== make frame! [
    series: ~
    value: ~
    part: ~
    dup: ~
    line: ~
]

While fetching such variables should be an error by default, I've also said that I like the idea of this being an easy, legal assignment... so you can say (var: ~) to unset it.

I've also become attached to saying that true and false are actually WORD! isotopes, and although you can't put isotopes in blocks...you can freely assign these to variables, and retrieve them from variables.

>> flag: true
== ~true~  ; isotope

>> flag
== ~true~  ; isotope

>> reify flag
== true

>> meta flag
== ~true~

It would be annoying if you could not directly assign a ~true~ or ~false~ isotope to a variable in a frame, and had to use some kind of ^META parameter convention to process it.

Yet I'm suspicious of being able to put parameter packs (isotopic blocks) or isotopic errors into a variable... ever:

>> ~['10 '20]~
== ~['10 '20]~  ; isotope

>> value: ~['10 '20]~
== 10

>> set/any 'value ~['10 '20]~
== ~['10 '20]~  ; isotope (sketchy!)

>> :value
== ~['10 '20]~  ; isotope (sketchy!)

It is likely the case that isotopes fit into categories of things that can be assigned to variables literally, and things that cannot. Keeping with the terminology, these might be called stable isotopes and unstable isotopes. And the only way you can capture an unstable isotope is to transform it into some stable form, e.g. with META or REIFY:

>> value: meta ~['10 '20]~
== ~['10 '20]~

Yet recall that with ~ and ~true~ and ~false~ (and ~_~ for null), it would seem we have a proof case that stable isotopes exist.

But there are other stable isotopes. e.g. now ACTION! isotopes are used to be the form of action that runs from a variable reference. Hence by definition you need to be able to store these in variables, and they are stable also.

How about splices? Is there any great reason to make them unstable? The following seems useful...

>> data: spread [d e]
== ~(d e)~  ; isotope

>> append [a b c] data
== [a b c d e]

And if you're going to specialize a function like APPEND, shouldn't you be able to just write:

apde: specialize :append [value: spread [d e]]

And this is where I was talking about the hassle of forcing these isotopes to be communicated by the ^META protocol, because if it meant you would sometimes take splices, you would always need to quote values you were appending... that was a headache. So we want as few of these ^META functions as we can possibly have.

Yet fetching without complaint makes it very easy to unintentionally get effects you weren't intending, like if you didn't realize DATA was a splice isotope and expect exactly two items here:

>> reduce [1 data]
== [1 d e]

We might say that the table stakes for showing you are aware that something "weird" is going on is on the access side, to use a GET-WORD!

>> reduce [1 :data]
== [1 d e]

Yet do note that if DATA were bound to a function, that function could return an isotope like SPREAD does, putting you back at the point of being unable to tell from the callsite how many elements will be resolved to.

Hm. Well, needing to use a GET-WORD! doesn't feel all that oppressive, and it offers some light protection that historical precedent has also offered (if a function in R3-Alpha/Red returns an UNSET! that's not an erroring condition, but if you get an unset out of a variable it will error without a GET-WORD!)

This Means ^META Arguments Must Be Special

I suggested this:

But now I'm saying that really there are just more isotopes in the stable family, communicated "as-is" to functions.

Yet when unstable isotopes are needed to be considered as parameters, the inability to encode them in variables "as is" would necessitate passing them via ^META conventions.

Consider RETURN. There is a RETURN/FORWARD that if you are returning a pack (isotopic block) actually returns the entire isotopic block, not just the first value. So this falls under the "can't do it without meta" category.

What about SET? At first it would seem to not have the problem, because it's setting a variable and everything it can take is representable in a variable (by definition).

But... if we leave ~ isotopes as the "I didn't specialize that" state of frames, and if SET doesn't take its argument as a ^META parameter, then there's no way to SET something to an unset state... without some mitigation.

Possible mitigations:

  1. just make UNSET a distinct native that's a completely distinct entry point, and argue you don't use SET to unset variables with a function

  2. during typechecking, nihil arguments are left as nihil if they're not refinements, vs. being set to null

    • this means if you're trying to set a variable's state from a refinement instead of a normal argument, you would not get full coverage of all possible variable states

    • this also means that if a normal argument is marked as being optional/nullable, then not acting on it when doing a MAKE FRAME! would not wind up with it being null as default

  3. make the parameter ^META...then let SPECIALIZE compensate for it normally. But when you want to write an UNSET function that is implemented as a specialization over SET, you build the frame directly and pass the value via ^META conventions

    3a. Have a more foundational SET* that takes a ^META argument and lets you unset things, but the plain SET takes a normal argument and does not

I'm rather attached to (var: ~) to unset things, so I feel (1) would be inconsistent with that mechanic.

With (3a), having SET* feels like a bad precedent. It may seem like it's making life a little easier for some people who are writing ADAPT...but I don't think the net benefit outweighs making it a ^META argument.

(2) sounds good in theory, if we're just making a frame and DO'ing it:

>> f: make frame! :set
== make frame! [
    target: ~
    value: ~
    groups: ~
]

>> x: 10
>> f.target: 'x
>> do f
>> x
** Error: x is ~ isotope (unset)

But if you try to turn a frame into an ACTION! and leave fields as ~, it treats them as unspecialized. This is designed to make it easier to write custom specialization functions, built on top of frame mechanics.

It's annoying that this is in the vein of exact problems ^META exists to solve...but you're having to shift to the meta convention to get coverage for just one isotopic state, which almost no one will be specializing functions to use! :frowning:

Annoying though it may be, I think it's going to have to be how it's done. If this is the biggest problem the whole thing has, then it's not really that bad!

2 Likes