One of the immediate incompatible changes people notice in Ren-C is the locking of source. They go to type append [a b] 'c
and it gives back an error that the series is "source or permanently locked".
It was motivated by a VERY good reason. To refresh your memory, here is an example of "objectively bad default behavior" the change was attempting to address:
symbol-name: function [symbol [word!]] [
switch symbol [
'+ ["plus"]
'? ["question-mark"]
...
]
]
...
filename: append (symbol-name '+) ".dat"
This innocent-looking piece of code has a terrible bug. The string you return lives in the SWITCH, so it's actually mutating the string inside the SWITCH. Every subsequent call to SYMBOL-NAME will be affected.
I feel like it shouldn't be controversial to say it should not be this easy to write self-modifying code on accident. Something exactly like this caused a problem in the build system that took me hours to find.
If Rebol is supposed to be more than a toy, it needs answers for usage problems like this--where it is very notably more brittle than other languages.
I knew locking source permanently was heavy-handed...
...but I want the example above to cause an error, vs. silently modify the string in the body of the function. Locking source was the only option we had at the time, and I figured we'd be on a better path to figuring out an answer if we biased it to be an error. It also meant we got more testing of the places in the code which make modifications to check the existing flags. (PROTECT in R3-Alpha was quite half-baked...but no one noticed how buggy it was because it wasn't used much.)
I've suggested that we need a lighter form of lock...something that doesn't make all views of a series have to be unchanging for all time, but that different views of a series be read or write.
Meet CONST and MUTABLE
Just in time for Christmas, we have a pioneering new feature of values being able to be read only or not. You can flip the bit yourself with the CONST and MUTABLE functions:
>> data: mutable [a b c]
== [a b c]
>> data-readonly: const data
== [a b c]
>> append data-readonly 'd
** Access Error: value is CONST (see MUTABLE): [a b c]
>> append data 'd
== [a b c d]
>> data-readonly
== [a b c d]
So it really is different from locking a series. For instance: you can keep write access for yourself, while giving out const access to subroutines you don't want to be doing casual modifications.
But the real win here is that the execution of code defaults to putting a wave of constness on any slots the evaluator fills from "literals"...be those blocks or strings. You see it catching the bug I introduced at the beginning of the post, of the string being changed inside the switch:
>> filename: append (symbol-name '+) ".dat"
** Access Error: value is CONST (see MUTABLE): "plus"
What's the difference between this and before?
Previously source was locked by LOADing and phases prior to a DO. Now, nothing gets const
-ed unless it runs. It is the "wave of evaluation" that brings along the constness, and any literals seen are affected.
This means some things won't work that did before. For instance, while this will work:
block: copy []
append block <works>
You no longer get a pass on a block that's been put elsewhere (e.g. by a compose) if it looks literal by the time the evaluator sees it, e.g.
block: copy []
do compose [append (block) <fails>]
After the compose, all the evaluator sees is append [] <fails>
. It doesn't care where that block came from--copied or not, it looks like you're trying to append to a literal. If you produce situations like this deliberately, you'll need to change them to:
block: copy []
do compose [append (mutable block) <fails>]
Or...
block: copy []
do compose [append mutable (block) <fails>]
Or...
block: mutable copy []
do compose [append (block) <fails>]
Any of which you could omit the copy from, if you didn't want a copy.
With DO MUTABLE...emulate Rebol2/R3-Alpha/Red!
There's a secret weapon for compatibility, which is that the way the constness propagates is based on a combination of bits on the values, and on inheritance through the call stack.
A simple use of DO MUTABLE shows you can get away with old-style behaivor:
>> do [append [1 2 3] 4]
** Access Error: value is CONST (see MUTABLE): [1 2 3]
>> do mutable [append [1 2 3] 4]
== [1 2 3 4]
But when execution happens under a mutable "evaluation wave", interpreted functions remember that fact.
>> do [newstyle: function [] [b: [1 2 3] append b 4]]
>> do mutable [oldstyle: function [] [b: [1 2 3] append b 4]]
>> newstyle
** Access Error: value is CONST (see MUTABLE): [1 2 3]
>> oldstyle
== [1 2 3 4]
So you can call a module written to Rebol2 conventions from Ren-C conventions. Moreover, even though the Rebol2-style module will have its series mutable by default, series you pass it from Ren-C code still get the protections:
>> do mutable [oldstyle: function [b] [clear b]]
>> oldstyle [1 2 3] ; oldstyle called from newstyle context
** Access Error: value is CONST (see MUTABLE): [1 2 3]
This concept of explicit mutability can be applied anywhere, at the branch-level if you like.
>> condition: true
>> either condition (mutable [append [] <success>]) [append [] <fail>]
>> condition: false
>> either condition (mutable [append [] <success>]) [append [] <fail>]
** Access Error: value is CONST (see MUTABLE): []
What should be the default?
The SWITCH case I opened with shows why I absolutely think that constness-on-evaluation is the right choice. That's in addition to addressing the speedbump every new user has when they write loop 10 [block: [] ...] and expect block to be reinitialized each time through the loop.
I don't think saying MUTABLE [...] is much of a burden to get deep mutable access to a series when you mean that.
I feel it's better to teach good habits early on. But who knows. Certainly in the console mutability has been the status quo. If modules enforced it by default but the console didn't, would that be a good tradeoff...or just confusing?