OpenSCAD2 supports "object inheritance": deriving a new object from an existing base object, by overriding some fields and adding new fields. This is an advanced feature that most users will not need. However, it is required for backwards compatibility.
The original language supports "script inheritance" by composing the include
statement with the ability to override definitions. These are low level features
that result in unexpected behaviour that is difficult to understand,
as can be seen in the wiki and in some forum posts.
These features are also buggy.
OpenSCAD2 takes a higher level approach, encapsulating each idiom combining include
and override as a high level feature with better semantics, a clearer mental model,
and better error messages if you mess up.
OpenSCAD has some features that cause great confusion for new users of the language.
- A name can be defined twice within the same block. The second definition overrides the first. The resulting behaviour is very strange.
- A reference to an undefined binding generates a warning, but not an error.
The binding evaluates to
undef
, and then unexpected behaviour can follow.
This is legal in the original language:
x=1;
echo(x); // ECHO: 17
x=17;
echo(x); // ECHO: 17
With its C-like syntax, OpenSCAD tricks the new user into thinking that the language supports re-assignable variables, and it almost seems to work, but something isn't right here...
In OpenSCAD2, this program is an error: duplicate definitions.
This "override" feature in the original language is useful, but only when
combined with include
.
- You write a script that defines some parameters with default values, then includes a second script which overrides some of these parameters and defines new parameters.
- You include a script (eg, a reusable library or model script), then override some of that script's top level definitions.
These idioms are supported in OpenSCAD2 by new features that make the override semantics explicit.
OpenSCAD1 gives warnings (but not errors) about undefined bindings:
- WARNING: Ignoring unknown variable 'x'.
- WARNING: Ignoring unknown function 'f'.
- WARNING: Ignoring unknown module 'm'.
The fact that the script is still executed when there are undefined bindings
is a source of confusion and forum posts, as some people try to understand the
resulting behaviour. For example, the
user manual entry about include
explains how, when overriding a variable in an included script,
you must sometimes put the override before the include,
and sometimes put it after the include.
(Search for "j=4" in the linked document.)
In fact, the necessity to put the override before the include
only happens if the variable being overridden is referenced but not defined
in the script being included.
The new implementation of OpenSCAD will promote these warnings to errors in both OpenSCAD1 and OpenSCAD2 modes.
There are situations where it makes sense for a script to reference
names that it doesn't define. Such a script is not intended to stand alone.
Instead, it is only intended to be include
d by another script, which supplies
the missing bindings.
These scripts are called mixins. In OpenSCAD2, they are identified by
special syntax which make explicit the missing bindings required by the script.
OpenSCAD 2015.03 has a bug related to include
and override.
include <foo> // has parameters 'x' and 'y'
x = 1; // override x
a = x + 1;
y = a + 1; // override y.
This code will emit a "WARNING: unknown variable a", then it will run and set y to undef. (This problem has been discussed in the forum. Google this: "The last value assigned to j is 4 and indeed the echo shows that, so why is k assigned undef? Seems like a bug in OpenScad".)
The key to fixing this bug is to understand that OpenSCAD is a declarative language
(not an imperative one), that id=expr;
is a definition (with no side effects),
not an assignment statement, and that therefore,
definitions do not need to be evaluated in the same order they are written.
The bug can be fixed in two ways:
- To reorder definitions into an order where each value is computed before it is needed, using a topological sort on the dependencies of each definition. This happens at compile time.
- To use lazy evaluation: definitions are not evaluated until the first time their value is needed, and then the result is cached. This happens at run time, and is more powerful, since it is guaranteed to find an evaluation order, if it exists, even if it is data dependent.
My current plan is to use lazy evaluation for evaluating scripts.
The include
and overlay
operators
provide general support for object inheritance:
the ability to derive a new object from an existing base object
by overriding existing fields and adding new fields.
This is analogous to inheritance in a class-based object oriented language.
This is in constrast to customization,
which uses the syntax
object(name1=value1, name2=value2, ...)
to override object parameters with new values.
Customization is a more limited operation
that is analogous to invoking a constructor in a class-based OOP language.
Two things you can't do with customization,
that you can do with include
and overlay
:
- You can't add new fields to the object.
- You can't create a dependency between two fields in the new object, such that customizing one field updates the other.
The overlay
operator customizes fields within a base object,
and adds new fields and geometry, as specified by an extension object.
If base
and extension
are both objects,
then base overlay extension
customizes the base object with those fields in extension
that are also in base
,
and extends the base object with those fields in extension
that are not in base
.
The geometry within extension
is added to the end of the base's geometry list.
The result is a new object.
If base
is a shape or a list of shapes and objects, then it is first converted to an object.
This could be used to add metadata to an existing shape or object.
For example,
material(x)(shape) = shape overlay {$material = x;};
material("nylon") cube(10);
If there are dependencies between fields in the extension object, then those dependences are preserved in the derived object. For example, in
base overlay {x=1; y=x+1;}
then regardless of what the base
object contains,
the derived object will contain two fields x
and y
,
such that customizing x
will update y
based on the new value of x
.
The overlay
operation described in the previous section
is limited by the fact that the extension object cannot access
fields in the base object. This limitation is overcome
by using a mixin in place of an extension object:
base overlay mixin
.
Mixins are described here.
include object;
includes all of the fields and geometry
of a specified base object into the current object under construction.
The object
argument is a compile time constant.
overlay object;
does the same thing, with the following differences:
- It's an error for a binding imported by
include
to conflict with another definition orinclude
in the same block. Just as definitions within a script are order independent, the location and order ofinclude
statements is not important. - The
overlay
statement takes either an object or a mixin argument, and allows new bindings imported from the object or mixin to override earlier bindings of the same name. The script
{..; overlay M1; ..; overlay M2; ..;}
is equivalent to
{..; ..; ..;} overlay M1 overlay M2
.
In OpenSCAD2, one definition cannot override another definition of the same name unless this is made explicit in the source code. Otherwise, it is an error.
Therefore, in order to translate an OpenSCAD1 include <file>
statement
into OpenSCAD2, you need to make explicit any overrides that are occurring.
In general, include <file.scad>
can be translated in two ways:
include script("file.scad");
— the normal caseoverlay script("file.scad");
— iffile.scad
overrides previous definitions
In OpenSCAD1, you can include a script, then customize that script by overriding some of its definitions. For example,
include <MCAD/bearing.scad>
epsilon = 0.02; // override epsilon within bearing.scad
In OpenSCAD2, the scope of an override must be made explicit. You must specify which included script is being overridden.
Normally, customization is all you need:
include script("MCAD/bearing.scad")(epsilon=0.02);
In more complex cases, you might need to use overlay
to customize the included script.
Eg,
include script("foo.scad") overlay {
...multiple override definitions that depend on each other...
};
Note that you can also customize a library script referenced by use
,
something not possible in OpenSCAD1.
use script("MCAD/bearing.scad")(epsilon=0.02);
In OpenSCAD1, library scripts are referenced using either use
or include
,
depending on how the library script is written. You may need to read the
source to figure out how to reference it. In OpenSCAD2, use
is recommended
for referencing all library scripts, while include
is only needed
for special purposes.
Inclusion is really a form of object inheritance.
The use cases for include
in OpenSCAD2 are narrower than in the original language:
- A model script includes another model in order to extend it with
new fields and geometry. That's an exceptional case.
More commonly, you just reference the other model, as
lollipop;
instead ofinclude lollipop;
. - A library script includes another library, for the purpose of
extending the other library's API. That's an exceptional case.
More commonly, you just use the library, as
use library;
.
See Library Scripts for more discussion of use
.
In the original language,
it is possible to write an OpenSCAD script
that is not intended to stand alone.
Instead, it is only intended to be include
d by another script.
These scripts are called mixins.
A mixin script may:
- Refer to bindings that it does not define. The including script is intended to supply these bindings.
- Override bindings whose default values are set by the including script.
For example,
dibond_config.scad
in the Mendel90 project
is a mixin script that does both of these things.
It's only designed to work when included by
config.scad
.
What does a mixin script denote? It doesn't denote an object: that doesn't make sense when some of the fields depend on undefined bindings. Instead, a mixin script denotes a mixin value, which contains incomplete, unevaluated code.
In OpenSCAD2, mixins are first class values
that specify a set of customizations and a set of extensions
that can be applied to a base object using the overlay
operator.
- In their most general form, mixins are constructed using the
mixin
keyword. This syntax adds two features missing from mixin scripts in the original language: the ability to refer to the base object when overriding a field definition, and the ability to declare the fields that the base object must contain (and optionally provide default values). - As a special case, objects can be used as mixins.
An overlay object can override
existing fields in the base, and add new fields and geometry.
See
overlay
operator.
The first statement in a mixin script is a require
statement,
which lists the mixin's prerequisites. When the script is evaluated,
the result is a mixin, instead of an object.
A require
statement has the syntax require (prerequisites);
.
The prerequisites is a comma separated list specifying which fields
must be defined by the base object.
A prerequisite is either id
or id=value
; in the latter case,
you are suppying a default value.
Following the require
statement are statements
that override existing bindings,
and add new bindings and geometry.
All pre-existing fields in the base object that are referenced
by the mixin script should be listed in the prerequisites,
otherwise you'll get an error about an undefined name.
To override a field in the base object, you need to use an override
definition, which is just a regular definition prefixed with the override
keyword. Within the body of an override definition,
the special variable $original
is bound to the original value in
the base object field that is being overridden.
$original
allows the new field value to be defined in terms
of the base field value. This is particularly useful
when overriding functions.
A mixin is applied to a base object using an overlay expression:
base overlay mixin
returns the derived object.
The overlay
operator is associative, thus (obj overlay mixin1) overlay mixin2
is equivalent to obj overlay (mixin1 overlay mixin2)
.
Thus you can combine two mixins using overlay
.
The overlay statement overlay mixin1;
is a variant of include
that applies the mixin to all definitions in the script before
the overlay
statement.
{include Object; overlay Mixin;}
is equivalent toObject overlay Mixin
.{include Mixin1; overlay Mixin2;}
is equivalent toMixin1 overlay Mixin2
.- When using the statement form, the mixin argument must be a compile time constant, whereas the expression form is more general, since it works on run time values.