Skip to content
Torsten Paul edited this page Jan 21, 2020 · 3 revisions

This is proposal for a new feature of the OpenSCAD language, there's no implementation yet.


Mutable Variables in OpenSCAD

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.

Version 2.0

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:

  1. 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.
  2. 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 one yield 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 form x or x[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.

Examples

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;
       }
   }
]; 

Design Considerations

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; and x := 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 why x = 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.
Clone this wiki locally