Skip to content

Latest commit

 

History

History
636 lines (545 loc) · 26.2 KB

Inheritance.md

File metadata and controls

636 lines (545 loc) · 26.2 KB

Object Inheritance

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.

The Problem

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.

Duplicate Definitions

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.

Undefined Bindings

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 included 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.

A Bug, Fixed by Lazy Evaluation

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.

Include and Overlay

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.

overlay

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.

overlay with a mixin

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.

the include and overlay statements

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 or include in the same block. Just as definitions within a script are order independent, the location and order of include 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 case
  • overlay script("file.scad"); — if file.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 of include 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.

Mixins

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 included 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.

Constructing a Mixin

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.

Applying a Mixin

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 to Object overlay Mixin.
  • {include Mixin1; overlay Mixin2;} is equivalent to Mixin1 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.