Case Study: LET vs. auto-gathered SET-WORD!

We now have a clunky implementation of LET (when I say clunky, I mean it's basically like how SET-WORD!s were gathered before...so no clever virtual binding just yet). I explain in that pull request what the difference virtual binding would make would be; you would have to be -running- a let in order to see its impacts. Simple inert references in blocks would not be enough to cause memory to be allocated for a variable. It would have to be run, at which point there would be an allocation and then a "wave" of binding traveling along with the evaluation stream.

But the clunky method used for the moment is at least good enough to get us started moving away from SET-WORD! gathering, and assess the approach. What I want us to do is to slowly start turning FUNCTIONs into FUNC, and see what problems we find (other than the known issue with PARSE rules like copy x: to end not having a place to put a LET). Then someday we deprecate FUNCTION. And then someday after that, they become synonyms.

Here is a sample function, just to look at the BEFORE and AFTER (of a kind of klutzy piece of code used by the help system on derived functions, which is not particularly great to uphold as great code but it's a real thing to look at):

Before

dig-action-meta-fields: function [value [action!]] [
    meta: meta-of :value else [
        return make system/standard/action-meta [
            description: _
            return-type: _
            return-note: _
            parameter-types: make frame! :value
            parameter-notes: make frame! :value
        ]
    ]

    underlying: try ensure [<opt> action!] any [
        select meta 'specializee
        select meta 'adaptee
        first try match block! select meta 'chainees
    ]

    fields: try all [:underlying | dig-action-meta-fields :underlying]

    inherit-frame: function [parent [<blank> frame!]] [
        let child: make frame! :value
        for-each param words of child [  ; `for-each param child` locks child
            child/(param): maybe select parent param
        ]
        return child
    ]

    return make system/standard/action-meta [
        description: try ensure [<opt> text!] any [
            select meta 'description
            copy try select fields 'description
        ]
        return-type: try ensure [<opt> block!] any [
            select meta 'return-type
            copy try select fields 'return-type
        ]
        return-note: try ensure [<opt> text!] any [
            select meta 'return-note
            copy try select fields 'return-note
        ]
        parameter-types: try ensure [<opt> frame!] any [
            select meta 'parameter-types
            inherit-frame try select fields 'parameter-types
        ]
        parameter-notes: try ensure [<opt> frame!] any [
            select meta 'parameter-notes
            inherit-frame try select fields 'parameter-notes
        ]
    ]
]

After

(Note: While the name is FUNC for the moment--to indicate suppression of auto-gathering--long term it could be FUNCTION as a synonym, under the current plan.)

dig-action-meta-fields: func [value [action!]] [
    let meta: meta-of :value else [
        return make system/standard/action-meta [
            description: _
            return-type: _
            return-note: _
            parameter-types: make frame! :value
            parameter-notes: make frame! :value
        ]
    ]

    let underlying: try ensure [<opt> action!] any [
        select meta 'specializee
        select meta 'adaptee
        first try match block! select meta 'chainees
    ]

    let fields: try all [:underlying | dig-action-meta-fields :underlying]

    let inherit-frame: func [parent [<blank> frame!]] [
        let child: make frame! :value
        for-each param words of child [  ; `for-each param child` locks child
            child/(param): maybe select parent param
        ]
        return child
    ]

    return make system/standard/action-meta [
        description: try ensure [<opt> text!] any [
            select meta 'description
            copy try select fields 'description
        ]
        return-type: try ensure [<opt> block!] any [
            select meta 'return-type
            copy try select fields 'return-type
        ]
        return-note: try ensure [<opt> text!] any [
            select meta 'return-note
            copy try select fields 'return-note
        ]
        parameter-types: try ensure [<opt> frame!] any [
            select meta 'parameter-types
            inherit-frame try select fields 'parameter-types
        ]
        parameter-notes: try ensure [<opt> frame!] any [
            select meta 'parameter-notes
            inherit-frame try select fields 'parameter-notes
        ]
    ]
]

Observations

The auto-gathered version may seem cleaner to you, because you're not needing to mark the locals explicitly, but that comes at a cost.

What cost? Well, how big is the frame for the first case...?

>> words of make frame! :dig-action-meta-fields  ; BEFORE
== [return value meta description return-type return-note parameter-types
    parameter-notes underlying fields inherit-frame child]

...and the second case?

>> words of make frame! :dig-action-meta-fields  ; AFTER
== [return value meta underlying fields inherit-frame child]

Here we have 4 unnecessary variables, and it's not just about wasted memory and processing cycles. This is why people who were skeptical of FUNCTION preferred FUNC and explicit locals. But it seems to me that LET offers the best of both worlds.

Like I say, this is not to say that an auto-gathering mechanism for SET-WORD! shouldn't exist. It's the kind of thing someone should be able to dream up here in the Minecraft-Of-Programming. But it's not what we want in the mezzanine and core implementation code. So we should let those who want it get it out of some third-party module or extension of some kind.

Thoughts on Unbinding

For reasons of safety, I'm wondering if any SET-WORD! in the body of the function be unbound if it isn't an argument, in the "wave" of a LET, or explicitly <with>'d in the spec.

y: <global>
foo: func [x] [
    y: x + 1
]
foo 10  ; error

bar: func [x <with> y] [
    y: x + 1
]
bar 10  ; sets y to 11

In the design I'm thinking of, it would only unbind the SET-WORD!s. e.g.

y: <global>
baz: func [condition] [
    print mold y
    code: [y: 11]
    if condition [do code]
]
baz false  ; would print <global>
baz true  ; would print <global> then error, y is unbound

Do people have any gut feelings on this? I feel like it would help catch bugs.

Unbinding set-words could really help to avoid bugs, it would lead to every accessed word having to be explicitly having to be defined as either global or local.

And from the ministry of crazy ideas: what if set-words were unbound and would be gathered as locals the moment they are accessed? Like, instead of erroring just make it local and keep going.

I think this is the better angle, and am looking into it. That said, what the locals-gathering abstraction does has applications as well. So it's sort of a matter of making sure people have choices.

(Rebol is the ministry of crazy ideas; I think the level to which this is "throw out every bit of conventional thinking" is oft underestimated.)