Haskell-Things / ImplicitCAD

A math-inspired CAD program in haskell. CSG, bevels, and shells; 2D & 3D geometry; 2D gcode generation...
https://implicitcad.org/
GNU Affero General Public License v3.0
1.39k stars 142 forks source link

function variable binding is not consistent with openscad #29

Closed tolomea closed 4 years ago

tolomea commented 12 years ago

In particular it looks like openscad is binding free variables at module invocation and implicitcad at module definition.

This scad:

function bob(x) = x*y; y=10; sphere(bob(1));

produces a sphere of size 10 in openscad and the following error in implicitcad:

Module sphere failed with the following messages: error in computing value for arugment r: Can't multiply objects of types Number and Undefined. Nothing to render

This scad:

y=10; function bob(x) = x*y; y=1; sphere(bob(1));

produces a sphere of size 1 in openscad and a sphere of size 10 in implicitcad.

I'm not sure how useful this behavior is, but if we were not going to support it we would need to come up with a good error message that catches both cases.

colah commented 12 years ago

I'm going to say this is clearly a bug in OpenSCAD. It's just a consequence of them not properly handling variables.

colah commented 12 years ago

Reopened per request for further discussion.

tolomea commented 12 years ago

I'm not sure I agree. Certainly what they do isn't unprecedented, I know several languages that bind like that. This snippet of Python for example:

bob=lambda x:x*y y=10 print bob(1)

It's also important to note that the example I gave is trivial and makes the OpenSCAD approach look silly, in more complex examples it's not as clear cut. Additionally I'd expect a non trivial portion of the models out there depend on this (even if not intentionally) and I'm wary of constructing barriers to adoption.

I found this from working through the OpenSCAD examples, here's example number 1:

module example001() { function r_from_dia(d) = d / 2;

module rotcy(rot, r, h) {
    rotate(90, rot)
        cylinder(r = r, h = h, center = true);
}

difference() {
    sphere(r = r_from_dia(size));
    rotcy([0, 0, 0], cy_r, cy_h);
    rotcy([1, 0, 0], cy_r, cy_h);
    rotcy([0, 1, 0], cy_r, cy_h);
}

size = 50;
hole = 25;

cy_r = r_from_dia(hole);
cy_h = r_from_dia(size * 2.5);

}

example001();

colah commented 12 years ago

I'll address OpenSCAD first. While its true that this introduces inconsistencies, it only does when the code is written in a strange manner. All variables in OpenSCAD are effectively constant, so it is very strange to not just declare them at the top. In fact, examples where this isn't done seem rather forced and can be trivially rewritten in another way.

The real issue is whether this is good or bad behaviour. Since you brought up Python, I'll use it to illustrate why I think the "use execution scope rather than declaration scope" approach is a poor choice. Please forgive me if this becomes a rant -- I've had a number of bugs in Python related to this sort of thing.

b=1
a=3+b
b=2
print a

Of course, a is 3+1=4. But what if a had been a function?

b=1
a= lambda x : 3+b
b=2
print a(0)

But now the result is 3+2. How does that make any sense?!? This is a very strange thing. Essentially, we are lazily interpreting functions, but not other values. And while laziness works fine in a functional language like Haskell, it leads to very strange results in an imperative language, where results depend on a state that has since changed.

We have arguments for when we want to pass values to the function for execution. I spent a good hour making sure functions could have as many arguments as one wanted. Let's leave scope for passing things to a function at declaration.

There are some times when special treatment of functions is nice -- like making recursion easy! -- but this seems silly.

tolomea commented 12 years ago

All variables in OpenSCAD are effectively constant.

function bob(x)=x*y; y=10; echo(bob(1)); y=20; echo(bob(1));

You are correct about that. I had misunderstood, I hadn't thought to try changing the value of a variable. That makes this a different type of beastie yet again.

so it is very strange to not just declare them at the top.

Actually it becomes irrelevant where they are declared. And copying OpenSCAD's behavior should be relatively easy. Alternatively if we wish to break from OpenSCAD's behavior we have the ability to produce comprehensive warnings about the matter.

Let's leave scope for passing things to a function at declaration.

If we were designing the language I would agree, but one of our main goals has to be ease of transition. The best option for ease of transition is conformant behavior, the fall back option is really through warnings so that updating code for our variant of the language is a mechanical process.

Essentially, we are lazily interpreting functions, but not other values.

This is off topic but: Not at all, it's clearly defined in imperative languages that the statements in a function are evaluated when the function is invoked. A function invocation in an imperative language is "go do that stuff now".

tolomea commented 12 years ago

Actually it becomes irrelevant where they are declared.

That is because in a system where variables can't be modified we are free to move them relative to the other content. Alternatively you could say that by definition variable sets are done first regardless of where they appear.

colah commented 12 years ago

Actually it becomes irrelevant where they are declared.

Well, except for things like readability.

And copying OpenSCAD's behavior should be relatively easy.

Mimicking OpenSCAD's behaviour would turn us into a preprocessor masquerading as a language. We would cease to be Turing complete.

Also, I've had at least three emails from people who are ecstatic about just this.

Alternatively if we wish to break from OpenSCAD's behavior we have the ability to produce comprehensive warnings about the matter.

Go on?

If we were designing the language I would agree, but one of our main goals has to be ease of transition.

One of them, but not at the cost of sacrificing real programming capacity.

A function invocation in an imperative language is "go do that stuff now".

But what that stuff is is decided at function execution, not declaration. Again, consider my numeric vs function example.

...

In any case, I spent some time asking people what the behaviour should be, to sanity check myself. The people I asked were fellow hacklab.toers or visiters and all had extensive programming experience. About half the people went one way, half the other (slightly more for declaration scope over execution scope). One person I spoke to was a language design PhD student and we talked extensively about it. He favoured the declaration scope behaviour.

In any case, we can address this more at a later date if you want. For now, I really need to focus on the rendering engine.

tolomea commented 12 years ago

Mimicking OpenSCAD's behaviour would turn us into a preprocessor masquerading as a language. We would cease to be Turing complete.

I don't really follow that. I'm not suggesting that we do only what OpenSCAD does, it is certainly ripe for improvement, but we need to be careful about changes that will prevent existing OpenSCAD models from working. There is a scale to this: 0: it works. 1: it produces a really clear error indicating exactly how to fix it. 2: it produces an incomprehensible error or no error. 3: it appears to work but the result is wrong.

Are you proposing removing the read only status of variables? That will create some type 3 issues. That could be addressed with an option or a porting tool that checks for problematic constructs in the OpenSCAD.

We should probably adopt another extension for our variant of scad, icad maybe. Otherwise we are going to have a lot of issues with people downloading stuff off sites like thingyverse and not knowing what tool they are expected to work in.

Also, I've had at least three emails from people who are ecstatic about just this.

About what?

Alternatively if we wish to break from OpenSCAD's behavior we have the ability to produce comprehensive warnings about the matter.

When variables are read only all we are doing is adding a rule that they need to be defined before use. With clear errors for attempting to write them and attempting to read them before they are defined this is a type 1 porting problem and should be relatively straight forward to deal with. If we remove the read only then we can't tell which assignments are intentional and which are accidental so we left with a situation where we just silently produce the wrong output for some small fraction of existing OpenSCAD files, like I said above, type 3.

One of them, but not at the cost of sacrificing real programming capacity.

Ideally what you want is 100% compatible with existing scad code with the addition of must have features. Of course the ideal is usually also impractical.

In any case, we can address this more at a later date if you want. For now, I really need to focus on the rendering engine.

If/when I dig into the rendering code the first thing I will do is render a point cloud of every evaluation of the implicit function overlaid on a transparent copy of the object. To give me a feel for what it is doing. I mention this just in case the idea is useful for you.

G

On Wed, Mar 7, 2012 at 5:45 PM, Christopher Olah < reply@reply.github.com

wrote:

Actually it becomes irrelevant where they are declared.

Well, except for things like readability.

And copying OpenSCAD's behavior should be relatively easy.

Mimicking OpenSCAD's behaviour would turn us into a preprocessor masquerading as a language. We would cease to be Turing complete.

Also, I've had at least three emails from people who are ecstatic about just this.

Alternatively if we wish to break from OpenSCAD's behavior we have the ability to produce comprehensive warnings about the matter.

Go on?

If we were designing the language I would agree, but one of our main goals has to be ease of transition.

One of them, but not at the cost of sacrificing real programming capacity.

A function invocation in an imperative language is "go do that stuff now".

But what that stuff is is decided at function execution, not declaration. Again, consider my numeric vs function example.

...

In any case, I spent some time asking people what the behaviour should be, to sanity check myself. The people I asked were fellow hacklab.toers or visiters and all had extensive programming experience. About half the people went one way, half the other (slightly more for declaration scope over execution scope). One person I spoke to was a language design PhD student and we talked extensively about it. He favoured the declaration scope behaviour.

In any case, we can address this more at a later date if you want. For now, I really need to focus on the rendering engine.


Reply to this email directly or view it on GitHub: https://github.com/colah/ImplicitCAD/issues/29#issuecomment-4363210

julialongtin commented 7 years ago

I'm actually good with leaving this in the type 3 state for now, but I'd prefer to move this to type 1.

julialongtin commented 5 years ago

A recent patch has changed this from a type 3, to at least a type 2, if not a type 1 error:

// Example8.escad -- variable assignment in loops.
a = 5;
for (c = [1, 2, 3]) {
        echo(c);
        a = a*c;
        echo(a);
}

now produces:

Warning at line 5, column 9: redefining already defined variable: "a"
Warning at line 5, column 9: redefining already defined variable: "a"
Warning at line 5, column 9: redefining already defined variable: "a"
TextOut at line 4, column 9: 1.0
TextOut at line 6, column 9: 5.0
TextOut at line 4, column 9: 2.0
TextOut at line 6, column 9: 10.0
TextOut at line 4, column 9: 3.0
TextOut at line 6, column 9: 30.0

Errors in general are in much better shape now. Anyone care to discuss what warnings/errors we would want to close this issue?

julialongtin commented 5 years ago

Giving this two more weeks, then closing.

julialongtin commented 4 years ago

I'm happy with the current behavior, closing.