Skip to content

OEP4: Unwrap Operator

Doug Moen edited this page Mar 13, 2015 · 3 revisions

The unwrap operator, &, is used to unwrap a list object and add each item into an argument list, in five different contexts.

This solves the following problems:

  • It provides a way to write functions with a variable number of positional parameters.
  • It provides a way to call a function with its parameter list specified by a list value, which will be needed once we can pass functions as arguments.
  • It generalizes list comprehensions so that the flatten function isn't necessary.
  • It provides a standard way to interpolate a group of shapes into a module's outer argument list, and allows us to deprecate intersection_for without breaking backward compatibility. So this is an alternative to the non-backward-compatible changes proposed by OEP2: Implicit Unions.

In a Function Definition

We need a way to write functions that accept a variable number of positional arguments. This is an advanced feature that will be primarily used by people writing library functions. Right now, a variadic function like append can only be implemented in C++, we can't define functions like this in the new "openscad/scad-utils" library. But we'd like to be able to do this.

This is done by using & to mark the final parameter in a function definition. The final parameter matches zero or more positional parameters, which are placed in a list.

function f(a, b, &rest) = ...;

With this definition, f(1,2,3,4,5) is equivalent to f(a=1,b=2,rest=[3,4,5]).

In a Function Call

Using the & operator, a list value can be unwrapped, and each element inserted as a separate positional parameter into a function call. For example, given x=[1,2,3], then f(0,&x,4) is equivalent to f(0,1,2,3,4).

Once again, this is intended as an advanced feature for library implementors. Consider that for generalized extrusion, we'd like to be able to pass functions as arguments to library functions. Let's assume that feature is available, and we have been passed a function f as an argument. We don't know how many arguments it takes (this can vary at each call), but we need to call it with an argument list args, which have also been passed to us. Here's how that is written:

function apply(f, &args) = f(&args);

In a List Literal

The unwrap operator can be used within a list literal. For example, [1,&[2,3,4],5] is equivalent to [1,2,3,4,5], and [&a,&b] is equivalent to concat(a,b).

In a List Comprehension

The unwrap operator can be applied to the final expression in a list comprehension. This makes list comprehensions significantly more powerful.

Here is how the concat function could be implemented as a library function (if it weren't already built in):

function concat(&args) = [for (a=args) &a];

Here's a useful idiom for using unwrap in a list comprehension for constructing arguments to polyhedron. Look at the vertices= and indices= lines:

r = 10;
h = 20;
w = 2;
s = 2;
step = 4;
vertices = [for (a=[0:step:360]) &[
  [r*cos(a), r*sin(a), 0],
  [r*cos(a), r*sin(a), h + s*sin(a*6)],
  [(r-w)*cos(a), (r-w)*sin(a), h + s*sin(a*6)],
  [(r-w)*cos(a), (r-w)*sin(a), 0]
]];
nv = 4*360/step;
indices = [for (a=[0:360/step]) &[
  [(4*a+0)%nv, (4*a+1)%nv, (4*a+5)%nv, (4*a+4)%nv],
  [(4*a+1)%nv, (4*a+2)%nv, (4*a+6)%nv, (4*a+5)%nv],
  [(4*a+2)%nv, (4*a+3)%nv, (4*a+7)%nv, (4*a+6)%nv],
  [(4*a+3)%nv, (4*a+0)%nv, (4*a+4)%nv, (4*a+7)%nv]]];
polyhedron(vertices, indices);

Without the & operator, the two arguments to polyhedron would need to be flattened (one level of list containment removed) using an auxiliary function:

function flatten(list) = [ for (i = list, v = i) v ];
polyhedron(flatten(vertices), flatten(indices));

In a Module Call

The & operator can be used to unwrap a group of shapes into a sequence of separate shape arguments, within the outer argument list of a module call.

This needs further explanation, so I'll define my terminology and give examples showing why this is useful.

Syntax of a Module Call

A module is a kind of function that takes shapes as arguments, and returns a shape as a result. Modules have two argument lists. The inner argument list is surrounded by parentheses, and the arguments are values. The outer argument list is surrounded by braces, and the arguments are shapes. For example, this is a module call:

my_module(1,2,3) {cube(3); square(2);}

In the above example, the inner argument list has 3 arguments, and the outer argument list has 2 arguments.

The syntax for module calls includes some handy abbreviations. If the outer argument list contains exactly one argument, then the braces can be omitted, like this:

my_module(1,2,3) cube(3);

If the outer argument list contains zero arguments, then the {} representing the empty argument list can be replaced by a ;, like this:

my_module(1,2,3);

Groups vs Shapes

A "group" is the OpenSCAD terminology for a list of shapes. A group can be passed as an argument to a module, and it can be returned as a result.

Here are some examples of OpenSCAD shape expressions (aka "statements") that return groups.

A for loop returns a group of len(list) shapes:

for (i=list) translate([i,i,i]) cube(2);

children(); returns the outer argument list of a module, as a group.

You can explicitly construct a group, to pass it as an argument or return it as a result, using the group module:

group() {
  cube(2);
  translate([1,1,1,]) cube(2);
}

Implicit Union

If a group is passed as an argument to a primitive module (like a transformation) then it is converted to a shape by an implicit union.

Examples

Sometimes, you want to take a group like children(), and pass each member of the group as a separate argument to another module. There is no standard, consistent way to do this in OpenSCAD, although there are specific kludges that work for specific modules. For example, to get the effect of passing children() as the argument list to intersection(), you can use this:

intersection_for(i = [0:$children-1]) { children(i); }

I'm proposing to provide the & operator to unwrap a group. It's exactly like unwrapping a list in a function call. So the above example would be more directly written like this:

intersection() &children();

And this means we no longer need the intersection_for module, because it can now be written like this:

intersection() &for(i=list) ...;

In OEP2: Implicit Unions, we consider the idea of automatically unwrapping groups whenever they appear as outer arguments in a module call, but that would break backward compatibility with perfectly reasonable programs like this:

intersection() {
    children(); // implicitly unioned
    sphere(10);
}

So this is an alternative to the parts of OEP2 that would break backward compatibility.

Comparison to Other Languages

In Python and Ruby, the unwrap operator is called *. But I am suggesting & instead, because * doesn't work in the context of a statement returning a shape. In that context, * already has the pre-existing meaning of "ignore the shape".

In Javascript, the arguments keyword returns a list of all arguments passed to a function. This mechanism could be used in OpenSCAD, but it would cause a problem.

Why? Well, I'm working on a plan to fix the problem where OpenSCAD fails to give error messages if you pass the wrong arguments to a function or module. For example, if you call cube(1,2,3) then you should get an error complaining that you passed too many arguments to cube. When you see the error message, you'll realize your mistake and change your code to cube([1,2,3]).

In order to make it possible to report errors when calling a user defined function (too many arguments, or too few arguments), we will compare the actual arguments that are passed to the function's formal parameter list, and if there is a mismatch, then we report an error.

But if the language has an arguments keyword like in Javascript, then this strategy will not work, because the formal parameter list won't provide accurate information about the minimum and maximum number of positional arguments. The only way to know what a legal argument list looks like is by reading the code and seeing how arguments is used by the code. It will then be much more difficult to automatically report errors for mismatched function argument lists.

So, even though the Python/Ruby solution might seem slightly more complicated for the authors of library functions, it will lead to a better experience for casual users who are using the library functions, because they will get better error messages if they pass the wrong number of arguments to a function.

Clone this wiki locally