Meta-Parameters And Function Composition

Right now (Note: original post written circa 2021) 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 converting things from antiforms into a non-antiforms, or adding quote levels. When you are assigning fields in the frame you have to remember to LIFT 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 [eval f]

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

Let's imagine they want to make it so that it tests all integers in a pack, if you give it a pack:

greater-than-10: func [^x [integer! pack!]] [
    if pack? ^x [
        ... code for testing if each element in pack > 10 ...
    ] else [
        ^x > 10
    ]
]

>> greater-than-10 20
== \~okay~\  ; antiform

>> greater-than-10 pack [20 30]
== \~okay~\  ; antiform

>> greater-than-10 pack [10 20]
== \~null~\  ; antiform

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 the callsites they knew about would remain working.

But it breaks our invocation via FRAME!.

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

>> eval f
** Error: X is a ^META argument, must be QUOTED! or QUASIFORM!

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

>> eval f
== \~okay~\  ; anti

UPDATE 2025: There is an awesome alternative to saying f.x: lift 20 or f.x: quote 20 which is now to use metavariables, e.g:

>> f.^x: 20
== 20

>> f.x
== '20
2 Likes

Higher-Level Functions than EVAL 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 hand back unstable isotopes to the caller. (Hence it calls its argument "ATOM" and not "VALUE").

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:

return-5: specialize return/ [atom: 5]

return-10-and-20: specialize return/ [atom: pack [10 20]]

So 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.

UPDATE 2025: It's unclear that SPECIALIZE hiding the fact that it's doing meta-assignments is a good idea. Anyone seeing xxx: pack [10 20] expects that assignment to decay.

Now that we have ^atom: pack [10 20] available, it makes much more sense that any assignment which is storing meta-representations be clear that it is doing so, and use such an assignment.

2 Likes

In 2022, I wrote: "In practice this 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."

That was before the maturation of modern isotopes...where unstable antiforms simply have no representation you can put in a variable.

As of 2025... to put forth the spirit of the proposal today, it would be:

"I think the frames should speak in a uniform meta-protocol. Then the mechanics that turn non-^META parameters into stable values should be done by the function when it's called."

Profound Idea :octopus: :bubbles: Profound Implications

This concept is that the central mechanics of function argument gatherering and dispatch always uses meta-parameterization, and it's only at that "last mile" of a FUNC or LAMBDA invocation that it does the convenience of un-meta-ing parameters (the ones you didn't mark as ^META) for you.

In the past I've complained about not wanting to make more ^META variables, because they are a a pain to deal with. For instance, if APPEND takes its value-to-append argument as ^META, you'd have to write:

append-reverse: enclose append/ func [f [frame!]] [
    if any-series? unlift f.value [
        f.value: lift reverse unlift f.value
    ]
    return eval f
]

I cited this kind of hassle as a reason why I was afraid to make F.VALUE meta, even though that would be the right answer to make it work with appending a VOID state that can't be put in a variable.

BUT I noted in the thread above the new power-tool of metavariables. That makes it better in more ways than one:

append-reverse: enclose append/ func [f [frame!]] [
    if any-series? f.^value [
        f.^value: reverse f.^value
    ]
    return eval f
]

That's a big improvement. And the peace of mind of knowing that APPEND can change its mind over time about whether it wants the variables to be meta or not when the function starts running... without affecting frames... is really nice.

No ^META Parameters In The Interface Is GOOD

There's tremendous benefit to removing the idea that a parameter is "meta" from a function's public interface.

HELP doesn't have to show it. Callers don't have to know it. They can just look at the type signature... if they see PACK! or something in there, then they know "oh it takes packs, may treat them specially".

But it's not just ENCLOSE that would be affected. Other function composition operations would too, such as ADAPT:

append-reverse: adapt append/ [
    print "Hi, I'm an adapted APPEND!"
    if series? ^value [^value: reverse ^value]
]

Basically, the adaptation runs on the frame before that "last mile" conversion. So it's subject to the same rules. (If we weren't going to make it subject to the rules, then ADAPT would have to be able to query the function it was adapting about the meta-parameter status. This would be like exposing it in HELP.)

It's a little bit sad that you wouldn't be able to just copy and paste code out of a FUNC body and put it into the code of an ADAPT. But I think the higher calling here is to say that the "meta vs. non-meta parameter convention" is just an implementation detail of things like FUNC and LAMBDA.

foo: func [x ^y] [...]

=>

foo: fundamental-meta-func [x y] [x: unlift x ...]

UPDATE: See the new theory of how you can just copy and paste: LIFT THE UNIVERSE

1 Like