Unbinding SET-WORD!s in FUNC: Pros and Cons

The ability to read from arbitrary words without saying you are going to do so is fundamental in Rebol:

foo: func [x <with> print] [
    print ["Having to say <with> print would be too annoying" x]
]

So you don't have to use <with> to import a lot of words. Yet I've long felt that being able to write arbitrary words is even more haphazard and problematic...

foo: func [x] [
    y: 10
    print ["Bad if you left off a LET or <local> on" y]
]

There's a certain amount of asymmetry, because a bad read is more likely to be noticed than a bad write.

Not only will reading a variable that doesn't exist cause errors, a variable that does exist will have weird content that you will notice going bad. But writing doesn't have that character...you can write into a "global" unintentionally and not notice for quite a long time. There are many bad consequences, e.g. two functions that seemed to work independently can suddenly trip over each other if called at the same time...if they both write globals they thought were local.

I've often thought it would be a good idea to unbind SET-WORD! in function bodies when that wasn't an explicit local or argument, or if it didn't have an explicit <with> or other importing construct like <in>.

Now that LET exists and offers a syntactically convenient way to make a new variable, this would call your attention to when you needed to add one.

y: 20
foo: func [x] [
    y: 10  ; this line would error in the proposal
    print ["Y is" y]
]

The idea would be that the error would guide you to either add <with> y to the function spec if you meant the existing global. Or to make Y a local with let y: 10 or adding <local> y to the spec.

The Downside: COMPOSE-ing Code With Bindings In It

Imagine if instead I had written:

 y: 20
 code: [y: y + x]
 foo: func [x] compose [
     ((code))
     print ["Y is" y]
 ]

It's important to point out: the idea that your bindings are going to be messed with is fundamental to using blocks of code as the medium in the first place. If you wanted hygienic behavior you would use functions...it's specifically the mingling of code that leads you to use COMPOSE in a situation like this.

But losing that binding on the SET-WORD! of Y is unfortunate for the code-builder. They presumably anticipated the impact on X in this case, but if the Y becomes unbound then they're going to have to somehow put a <with> y in the function spec as well. Should they have to?

The compromise might be that there's a wildcard form of WITH, a sort of "WITH-ALL".

Unfortunately WORD!s like * can be things you're actually doing <with>. So you can't mean this by <with> *. Also unfortunate is that since the spec uses tags, you can't say <with> <*>...which might be factored into arguments for why it should be a token instead so you could say #with <*>. Or it could be <with> #all

In any case...it's less burdensome to tell the person making the function to make it the kind of function that just allows all the bindings, rather than having to figure out how to transmit the name of each individual variable into it.

Implementing this is not as easy as it sounds (if it sounds easy). There are various layers going together that would have to analyze which bindings to rule in or out. But I just see way too many instances of bugs from writing to globals that meant to be local.

1 Like

Throwing a further wrench into the idea of unbinding SET-WORD!s is how to deal with SET-BLOCK!s.

[pos value]: transcode "whatever"

This is even more shaky... because people can put GROUP!s of code in these:

word: 'value
[pos (word)]: transcode "whatever"

So do you unbind only plain words? This really gets in the way of dialecting.

Anyway, this is just another dimension of pointing out how hard it is when going up against the fundamental idea of things preserving their binding.