Skip to content

OEP3: Function Values and Variable Scoping improvements

OskarLinde edited this page Jun 4, 2014 · 5 revisions

This is a suggestion to extend function and variable definitions to allow storing function references in variable, anonymous functions as well as allowing variable definitions in more scopes.

Discussions

Suggestions

Nomenclature

  • Variables -> Constants
  • Assignment -> Definition

Local Definitions

See #347

This allows definitions in any { } scope, and effectively deprecates assign().

Examples:

union() {
  a = 3;
  b = 4;
  square(b);
  circle(a);
}

When adding Function References and Lambda Expressions, we probably need to also allow function definitions in any scope which allows constant definitions. Special care should be taken with recursive functions:

n = 42;
{
  a = n; // a is 42
  function f(x) = if (x <= 0) 1 else x * g(x-1);
  function g(x) = if (x <= 0) 1 else x * f(x-1);
  n = f(5);
}

For this to work, value lookup inside function bodies needs to be recursive in nature. Late binding (variables are bound at call time), is an alternative in this case to to recursive lookup. The two methods could give different results in the following case:

n = 42;
{
    function f() = n;
    echo(f());
    n = 5;
    echo(f());
}

Function References

Allow to store a function reference in a constant:

myfunc = sin;
myfunc(45); // equivalent to sin(45)

..or pass as a function/module argument:

function myfunc(t) = let(turns=10, pitch=1) [r*cos(t*turns*360), r*sin(t*turns*3600), t*pitch*turns];
sweep(path=myfunc) circle();

For declaring function arguments inline the function call, one or more of the following syntaxes could work:

sweep(path(t) = rotation(t*45)) square();
sweep(path = function(t) rotation(t*45)) square();
sweep(function path(t) = rotation(t*45)) square();

Since functions and constants (and modules) currently all have their own namespace, we need to be careful about backwards compatibility. Two primary ways of dealing with this is to either merge namespaces or to have a smart ways of disambiguating expressions bound to the same name at lookup time.

Anonymous functions (aka. lambda expression)

function(<args>) <expression>

myfunc = function(x) x + 1

This is equivalent to function myfunc(x) = x + 1;

Further Ideas

These could be brought into scope:

Access to positional function arguments

To support functions with variable number of arguments, we could support accessing the argument list with a special variable, e.g. args.

Example:

function concat() = flatten([for (v=args) v]);

Named Let

Scheme has a feature called 'named let', which I would love to add to OpenSCAD. It's a language feature you won't know unless you use Scheme, but it is a good fit for OpenSCAD, because it addresses a common problem when defining functions that iterate using recursion.

Here is the standard tail recursive factorial function, written in idiomatic OpenSCAD:

  function fact(n, result=1) =
    n <= 1 ? result : fact(n-1, n*result)

I'm using the idiom where you define a function with extra parameters with default values; these parameters are only intended to be used internally. But it's messy, because it exposes implementation details to the outside world.

Here's the same function written using the 'named let' feature:

  function fact(n) =
    let f(n=n, result=1)
      n <= 1 ? result : f(n-1, n*result)

A 'named let' is just like a regular let, except that it binds an additional identifier (in this case, 'f') to a function. If you don't call the function, it behaves like a normal let expression. If you do call the function, then you iterate with a new set of values that are bound to the original local variables. The named let above is equivalent to the following syntax:

  function fact(n) =
    let (function f(n=n, result=1) = n <= 1 ? result : f(n-1, n*result))
        f();

There is one problem with the named let syntax: Anonymous let() offers sequential assignment while function calls use parallel assignment semantics. In order to make sense, named let() should use parallel rather than sequential assignment.

Disallow Duplicate Definitions

OpenSCAD silently allows duplicate definitions, but the semantics are pretty unintuitive, e.g. the following causes 4 to be printed twice:

  x=2;
  echo(x);
  x=4;
  echo(x);

The reason for this is that, on the cmd-line, we allow any variable to be reassigned. This is done by appending whatever expressions sent on the cmd-line to the OpenSCAD document in questions. To override global variables, the expressions need to be available for the entire document. If we change how we handle duplicate definitions, this common use-case needs to be supported.

Clone this wiki locally