-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Mutable Variables
This is proposal for a new feature of the OpenSCAD language, there's no implementation yet.
A number of people have requested that we add mutable variables to OpenSCAD, so that traditional imperative programming idioms can be used. There is certainly a demand for this feature, and based on forum discussions, it would make the language easier to use for people who are trying to port standard geometric algorithms into OpenSCAD.
However, the core team who lead OpenSCAD development have expressed a strong desire to preserve the pure, declarative semantics of OpenSCAD. It's for two reasons: to make the language simple and easy to understand for users (who might be designers, not programmers), and so that the implementation can take advantage of referential transparency.
It is possible to introduce imperative features into OpenSCAD, without violating the declarative semantics. General purpose functional programming languages have been doing this since the 1990's. Haskell has Monads, and F# has Computation Expressions.
The real question is whether we actually want to turn OpenSCAD into a general purpose functional programming language. I think that's an open question. When I first joined the community, it was often repeated that OpenSCAD is "not a programming language". It's just a simple declarative language for defining parameterized CSG models. How much complexity do we want to add in order to make OpenSCAD into a programming language?
This paper will help people explore the question. I describe a fairly complete set of extensions for adding mutable variables while preserving declarative semantics.
We extend OpenSCAD with an "imperative sublanguage".
The new keyword do
switches to the imperative sublanguage.
It is a language of statements that are executed for their side effects.
You can define mutable variables, reassign those variables,
and update arrays one element at a time.
We introduce two new top level language constructs:
- A new kind of expression/statement, called a "do block". It delimits a sequence of "imperative" statements which define mutable variables and operate on those variables.
- A new kind of definition, called a "procedure". It's analogous to a function, except that you can pass one or more mutable variables as arguments, by reference, and the procedure can update their values. A procedure doesn't return a result, except by modifying its arguments. A procedure can only be called from within a
do
block.
We introduce 4 new keywords: do
, var
, while
and procedure
.
A "do block" has the syntax do { statement1; statement2; ... }
.
The following general imperative statement types are supported within a do block:
var x; // define mutable variable x, initialize to undef
var x := 0; // define and initialize mutable variable
x := x + 1; // reassign an existing mutable variable
a[i] := x; // mutate a list element; a is a mutable list variable
p(v, 42); // call a procedure
{ stmt1; stmt2; ... } // compound statement
if (cond) stmt
if (cond) stmt else stmt
while (cond) stmt
for (..; ..; ..) stmt // C-style for, as currently implemented
for (i = list) stmt
echo(...);
assert(...);
A do block can be used in the following contexts:
- As an expression. In this context, it contains at least one
return
statement, which specifies the value of the expression. - As an element of a list constructor.
In this context, it contains at least one
yield
statement, which adds another value to the list being constructed, then continues execution. - As a geometry statement, at the top level of a script,
or within a module definition, or within a regular
{...}
compound geometry statement. In this context, it contains at least oneyield
statement, which adds another shape to the group being constructed, then continues execution.
The following special imperative statement types are supported:
return expr; // only legal when do block is used as an expression
yield expr; // only legal when do block is used as an element of a list constructor
yield module-call; // only legal when do block is used as a geometry statement
return; // only legal within a procedure definition
A procedure definition has the syntax
procedure name(param1, param2, ...) { statement* }
There are two kinds of parameter:
- If the parameter is declared
var <id>
, then the argument is a mutable variable passed by reference, of the formx
orx[i]
, which the procedure can mutate by assigning to the parameter. - If the parameter is declared
<id>
, then the argument is an ordinary expression, passed by value.
There are no optional parameters, default values, or keyword parameters.
A procedure is called using the syntax pname(arg1,arg2,...)
,
but only within an imperative context.
Here is an imperative style factorial function:
function factorial(n) = do {
var result := 1;
for (i = [1:n])
result := result * i;
return result
};
Here is an imperative style quicksort function:
function quicksort(list) = do {
var array := list;
_sort(array, 0, len(list));
return array;
};
procedure _sort(var array, left, right) {
var index;
_partition(array, left, right, index);
if (left < index - 1)
_sort(array, left, index - 1);
if (index < right)
_sort(array, index, right);
};
procedure _partition(var array, left, right, var i) {
i := left;
var j := right;
var pivot := array[floor((left + right) / 2)];
while (i <= j) {
while (array[i] < pivot)
i := i + 1;
while (array[j] > pivot)
j := j - 1;
if (i <= j) {
var tmp := array[i];
array[i] := array[j];
array[j] := tmp;
i := i + 1;
j := j + 1;
}
}
};
Here's an example of a do block appearing as an element of a list constructor.
It uses yield
to add elements to the array under construction.
It's an imperative style list comprehension.
function fibonacci(n) =
[
do {
var first := 0;
var second := 1;
for (i = [0:n]) {
var next;
if (i <= 1)
next := i;
else {
next := first + second;
first := second;
second := next;
};
yield next;
}
}
];
OpenSCAD remains a pure declarative language.
- Expressions and function calls remain side effect free.
Side effects (modifications to mutable variables) are tightly constrained
so that they aren't visible outside of a
do
block. - You can't define impure functions that map the same arguments onto a different result each time they are called, because it's impossible for a function to reference a global mutable variable.
You can't define a mutable variable at the top level.
A library can't export a mutable variable.
They can only be defined locally within a do
block.
You can't define functions, modules or immutable variables (like pi = 3.1416;
)
within a do
block. One reason: this makes it impossible for a function definition
to "capture" a mutable variable, and thus become an impure function.
Another reason: I can't reconcile this with the semantics of definitions in OpenSCAD2.
When a mutable variable is referenced in an expression, the current value is taken. In OpenSCAD2, this applies to anonymous function literals as well: the current value is captured each time the function literal is evaluated. (By contrast, in an imperative language, a function literal captures the variable, not the variable's current value, which results in an impure function.)
Since do
blocks can be used as expressions, and expressions can occur in a do
block, it follows that
do
blocks can be nested. However, a do
block can reference an external mutable variable only to read it, not to write it. Expressions cannot have non-local side effects.
The imperative sublanguage syntax is mostly C-like.
- There is a distinction between
var x := 0;
andx := 0;
. The first defines a new mutable variable, and establishes its scope as local to the smallest enclosing set of curly braces. The second reassigns an existing variable. This distinction is required in any imperative language that allows variables to be declared locally within a compound statement (like C and Javascript, unlike Python). - The
:=
assignment operator (from Pascal) is used rather than=
(from C), because this makes it very obvious when you are reading code in the imperative sublanguage. It's important to make this visual distinction to avoid confusion. Otherwise, we'll have constant forum questions about whyx = x + 1;
doesn't work here, when it clearly does work over there.
The imperative syntax is designed to rigidly partition imperative statements from declarative statements/expressions: there is no overlap, no context where either is allowed.
- The meaning of
{...}
in the imperative language is a C-like compound statement. In OpenSCAD2, the same syntax is an object literal, but there's no context where either could occur, so there's no ambiguity. - The reason procedures don't have a return value (other than modifying argument variables) is that I do not want a procedure call to be an expression. Expressions cannot have side effects.