Every newbie to Rebol (and every experienced user too!) gets bitten by the intrinsic mutability of source series. A common misunderstanding/mistake might look like:
blockify: func [x] [
block: []
append block x
return block
]
>> blockify 10
== [10]
>> blockify 20
== [10 20] ; !!! why didn't `block: []` reset the block?
Some have deemed it easy enough to learn to say block: copy []
. But consider the following:
symbol-name: func [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 the APPEND is 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 equivalent to this (but trickier) 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 notably much more brittle than other languages.
The APPENDs Above Must Fail, But By What Means?
I want those examples to cause errors, vs. silently modify the blocks or strings resident in the bodies of functions.
Yet a lot of off-the-cuff scripting (and test code) relies on the mutability of source, e.g.:
>> append [a b] 'c
== [a b c]
R3-Alpha had the concept of being able to PROTECT a series so that all references to it would be immutable. But if we were to make a rule that all source series were permanently locked, that would be a heavy-handed policy that wouldn't permit alternate styles of coding ever.
I concluded that we needed another--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. And constructs could fiddle this bit as they saw appropriate.
Meet CONST and MUTABLE
Ren-C's pioneering new feature is of values being able to be read only or not. You can flip the bit yourself with the CONST and MUTABLE functions:
>> data: [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]
>> append mutable data-readonly 'e
== [a b c d e]
It's quite 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"
The Constructs Are In Control
In this model, the constness is applied by anything that thinks of its argument as being iterative.
So for example, the WHILE loop takes its body (and condition) as a <const>
-marked parameter.
input: [a b c]
output: [] ; want to get [[a] [b] [c]]
while [item: try take input] [
block: []
append block item
append output block
]
You'll get an error on the APPEND to BLOCK of "CONST or iterative value"
.
By comparison, EVAL does not take its block argument as a const parameter, so this works without complaining about the appends to data:
>> eval [data: [], append data <1>, append data <2>]
== [<1> <2>]
But it's inherited, so a EVAL inside of a WHILE would have the block it received to do as const, due to the WHILE's influence.
Predicting that functions are likely to be called more than once, FUNC takes its body as CONST...and that constness propagates as the wave of evaluation proceeds through the body.
But notice that as long as the underlying series isn't immutable (due to things like PROTECT), you can subvert the const bit with MUTABLE:
accumulate: func [x] [
accumulator: mutable []
return append accumulator x
]
>> accumulate 10
== [10]
>> accumulate 20
== [10 20]
Emulating historical Rebol2/R3-Alpha/Red conventions just means tweaking the specs for things like FUNC and WHILE. Instead of taking their body parameters as <const>
, take them normally.
Should Modules Be Stricter By Default?
The SWITCH case I opened with shows why I absolutely think that constness-on-func-bodies is the right choice. That's in addition to addressing the speedbump every new user has when they write repeat 10 [block: [] ...] and expect block to be reinitialized each time through the loop.
But what should the default be for code that's not in a function or a loop?
Certainly in the console mutability has been the status quo. If modules enforced constness for their top-level code (despite being run only once) but the console didn't, would that be a good tradeoff...or just confusing?
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.