-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
OEP4: Unwrap Operator
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.
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])
.
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);
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)
.
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));
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.
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);
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);
}
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.
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.
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.