curv3d / curv

a language for making art using mathematics
Apache License 2.0
1.14k stars 73 forks source link

RFC: let_parametric and let_mutable #62

Closed doug-moen closed 5 years ago

doug-moen commented 5 years ago

These are the changes to Curv language syntax that I propose for release 0.4:

The goal for both of these syntax changes is to make the syntax more consistent and orthogonal. Local variables are now always defined using the word "let".

let_parametric

The let_parametric feature allows you to define "parametric shapes", with shape parameters that are bound to sliders and other graphical value pickers in the Viewer window.

The following program declares a shape parameter that is bound to a graphical slider widget:

  let_parametric
      size :: slider(1,5) = 3;
  in
  cube size

When you run this program, the Viewer window contains a slider that lets you vary the size parameter from 1 to 5, with an initial value of 3.

There are 3 associated language features:

predicate assertions

An expression of the form value :: predicate asserts that predicate(value) returns true, and then it returns value.

For example, x :: is_num returns x, after first checking that x is a number, and aborting the program if x is non-numeric. You could achieve the same thing by writing

do assert(is_num x) in x

name :: predicate is a predicate pattern that binds a value V to name, after first checking that predicate(V) is true. If the predicate is false, the pattern match fails.

For example,

let
    n :: is_num = f(x);
in use(n)

ensures that n is a number when it is defined.

For another example, given

cuboid =
    match [
    n :: is_num -> cube n;
    v :: is_vec3 -> box v;
    ]

then cuboid 3 is a cube of size 3, and cuboid[1,2,3] is a box of dimensions [1,2,3].

picker values

A picker value is a predicate function that specifies the type and range of values of a shape parameter that is associated with a graphical value picker. It also specifies what kind of value picker widget is used by the GUI.

Here are the currently supported picker expressions:

let_parametric

To add graphical parameters to a shape in a Curv program, you prefix a shape expression with a let_parametric clause:

  let_parametric <parameter1>; <parameter2>; ... in <shape-expression>

This is an expression that returns a parametric shape value.

Each <parameter> has the form:

  <identifier> :: <picker> = <initial-value>;

A let_parametric expression differs from a let expression in several ways:

let_mutable

This is a redesign of how mutable variables are defined and used.

The old syntax, which was an undocumented experimental feature, used the var keyword to define mutable variables inside of a do block.

The new syntax uses let_mutable to define mutable variables. With this change,

The ability to define mutable variables using var will be deprecated.

Mutable variables in an expression:

sum a =
    let_mutable
        i := 0;
        total := 0;
    in do
        while (i < count a) (
            total := total + a[i];
            i := i + 1;
        );
    in total;

Mutable variables in a list comprehension. In this case, we don't use do:

[ let_mutable i := 0; in while (i < 10) (i; i:=i+1;) ]
// => [0,1,2,3,4,5,6,7,8,9]

When multiple mutable variables are defined in a single block, the definitions are executed in sequence, and are sequentially scoped. The scope of each variable begins at the following definition.

p-e-w commented 5 years ago

It's good to see the syntax being clarified here, but I really dislike the proposed keywords let_parametric and let_mutable. They are long and unwieldy, and AFAICT would be the only language keywords containing an underscore.

I propose the following alternatives:

let_mutable -> let mut

This matches the syntax for mutable bindings in Rust. I like that this expresses how a mutable binding is a special case of a standard (immutable) one, not an entirely different entity. I also suggest eliminating the := operator for such bindings in favor of simply = like for immutable variables. The mut prefix already clearly expresses the semantics so there is no need for this extra syntactic difference.

Example of this proposal in action:

let
  a = 10;
  mut i = 0;
in do
  while (i < a) (
    i = i + 1;
  );
in i

Note how this syntax allows mixing mutable and immutable bindings in the same let block.

let_parametric -> let

Since in the above proposal, let_parametric bindings require a picker annotation, and picker expressions are built-in and well-known to the compiler, it seems that the _parametric suffix is entirely superfluous.

That is,

let size :: slider(1,5) = 3;

already fully expresses the fact that size is parametric, without the need for an extra keyword. Again, this has the considerable advantage that parametric and non-parametric (and mutable) declarations can all go into the same let block, which can massively reduce nesting in complex programs.

p-e-w commented 5 years ago

Continuation of the above:

Since in your original proposal, let_parametric and let_mutable impose constraints on the expression after the in keyword, it follows that if the three kinds of let blocks were unified, those constraints would have to be extracted from the individual variable bindings and jointly enforced.

That is, in the program

let
  a = 0;
  mut b = 0;
  c :: slider(0,3) = 0;
in
  <RHS>

<RHS> would have to be a do block (because of b) that evaluates to a record value (because of c). I can see how this might be more difficult to implement than just having three different block types where all variables are of the same kind, but it looks so much cleaner and simpler to use that if it is at all possible it still seems worth it.

p-e-w commented 5 years ago

One more argument for unifying the three block types: In certain (common) situations, your original proposal actually encourages a dangerous shortcut that sacrifices safety for brevity.

Let's say you have a value a that holds the number of iterations you need to do in a loop as in my let mut example above. Using the syntax of your original proposal, the "correct" way to write this would be

let
  a = 10;
in
let_mutable
  i := 0;
in
...

for an immutable a and mutable loop variable i. This is rather ugly and verbose, so it is quite tempting to change it to

let_mutable
  a := 10;
  i := 0;
in
...

which is shorter and cleaner – but a is now mutable even though we know it shouldn't be! So if accidentally we reassign to a in the do block, Curv will not complain.

The original proposal basically says "when declaring variables in a let block, either all of them must be immutable, or all of them must be mutable, or all of them must be parametric". I don't think this maps nicely to most people's mental model.

p-e-w commented 5 years ago

For complete clarity of what I am suggesting, here is a more formal description in terms of a rewrite rule, which should unambiguously define the semantics in the presence of mixed declaration types.


A let block of the form

let
  immutable_1 = 0;
  mut mutable_1 = 0;
  parametric_1 :: picker = 0;
  immutable_2 = 0;
  mut mutable_2 = 0;
  parametric_2 :: picker = 0;
  immutable_3 = 0;
  mut mutable_3 = 0;
  parametric_3 :: picker = 0;
in
  ...

is transformed into

let
  immutable_1 = 0;
  immutable_2 = 0;
  immutable_3 = 0;
in
let_parametric
  parametric_1 :: picker = 0;
  parametric_2 :: picker = 0;
  parametric_3 :: picker = 0;
in
let_mutable
  mutable_1 := 0;
  mutable_2 := 0;
  mutable_3 := 0;
in
  ...

and then your original proposal applies.


That is, the declarations are placed in three separate nested blocks according to their type, and in the order in which they appear in the original unified let block. Same semantics, but much better quality of life.

doug-moen commented 5 years ago

I appreciate the level of detail and clarity in your counterproposal.

I'd like to clarify some points in my RFC.

The RHS of let_mutable ... in RHS is either do <statement> in <phrase>, or it is <statement>. The mutable variables can be modified by the <statement>. I gave examples for both cases in the RFC.

In a definition of the form

name :: p = value

the expression p is a run-time expression. It could be a predicate function like is_num, or in the case of let_parametric, it could also be a picker predicate like slider(0,10).

In your proposal, if I write

    f p x =
        let
            N :: p = x;
        in cube N;

then the function argument p could be is_num, or it could be slider(0,10), this is not known until run time. But I need to know at compile time whether to generate code for a parametric shape, and there is no compile time syntax in this program that makes this distinction. So that is why I have a let_parametric keyword.

sebastien commented 5 years ago

@p-e-w agreed, I was actually going to say exactly the same thing (although, not with this impressive level of detail).

I personally see "mutable" and "parametric" as annotations on the slot declaration denoted by a let. By constraining the slot's value set using the :: operator and the slider() predicate, you implicitly declare it as parametric. You could then theoretically do let size :: parametric if such a predicate existed.

It is also good to note that if something is parametric, is also, by definition, mutable. So continuing on that train of thought, we could just as well have let size :: mutable.

Now, @doug-moen mentioned that the slot predicates are runtime only, but some core predicates might be known at compile time (mutable, parametric) and if the compiler is not able to figure out if a custom predicate transitively applies mutable or parametric, it's always possible to be explicit: let size :: mutable(my_custom_predicate(...)). The compiler could issue a hint along with the error message if someone tries to reassign a value to the slot (consider using value :: mutable(..)...)

The main positive point I see in favour of the let_{mutable,parametric} is that they are easy to parse and that tools (like a GUI) could extract the parameters by partially parsing the file. However, this is brittle and ultimately it's better if the curv compiler can give a list of the parameters that are used by the resulting GLSL code. Also, the fewer keywords in the syntax, the better.

To sum up:

doug-moen commented 5 years ago

Parametric variables are not mutable. They are like function parameters with default values. You cannot reassign them using the := assignment operator. That would not even make logical sense, because then each reference to a given parameter in a shape expression could have a different value.

sebastien commented 5 years ago

Right, I got confused by the fact that they are mutable from a user point of view, but not at all from the compiler perspective. That being said, it does not necessarily invalidate my point: parametric and mutable could be constraints/predicates bound to slots, achieving the same effect as let_{mutable,parametric} without introducing extra keywords.

I find the :: quite elegant in the sense that it is more generic than types (it allows for any expression to constrain the domain of assignable values) and could also work for compile-time information and annotation.

For instance, we could introduce control description:

let height :: slider(...) >> describe "Changes the height of the vase"

or maybe we'd like some presets:

let height :: slider (...) >> presets [["Small", 1.0], ["Large"' 10.0]]

I see the :: as a generic mechanism to inject both runtime and compile-time annotations, and it seems like it would scale with the needs of an interactive editing environment.

p-e-w commented 5 years ago

@doug-moen Thanks for the clarification. I didn't realize that you intend to allow picker annotations to be variables themselves.

FWIW, I feel that this is a case where the awe-inducing generality that Curv aspires to hurts its usability (#50 is another one; if Curv restricted arguments to lists those concerns would mostly disappear). It's cool in principle that such a thing is possible – but I can't think of a case where I would need it. It seems much more pragmatic to just say, if you want a parametric binding, you must declare it as

name :: picker = value;

where picker is one of a well-known set of picker types, not an arbitrary expression to be evaluated at runtime. In exchange, the simplification from my proposal becomes possible, which to me seems like a very sensible tradeoff.

One thing I'm confused about is that several of the above posts refer to a picker as a "predicate". I do not see how this is the case, or why it should be. In

a :: slider(0,5) = 0;

is slider(0,5)(a) == true? You can of course make slider(0,5) evaluate to a function that performs the range check, but I see a semantic overloading here. I think of slider as an annotation, not a predicate. Its prime purpose is not to place constraints on the value, but to attach metadata to the binding, which in this case is used to inform the creation of GUI widgets. As such, it might actually make sense to have a separate syntax for it, just like mut.

How about this revised proposal?

let
  [annotation] name [:: predicate] = value;
  ...
in
  ...

where annotation is either blank, or mut, or a picker literal, which fits well because variables cannot be both parametric and mutable. predicate can be any function value, including one only known at runtime.

The unified let syntax I proposed above remains possible, and additional possibilities become available by separating predicate and annotation. For example, you can now have a text "picker" that lets the user type arbitrary text input, but validate this input against an independent predicate.

p-e-w commented 5 years ago

Of course, my revised proposal is somewhat at odds with @sebastien's idea for adding data like a description to the picker. If picker annotations are required to be compile-time literals, they can't be created by passing a picker value through a function like describe, because the compiler cannot readily check that the result of that function is still a picker (unless that is also built-in, but I don't think that would be the right path).

Instead, such additional metadata would need to be directly supplied as an argument to the picker constructor itself, i.e. something like

slider {
  min: 0,
  max: 5,
  step: 1,
  description: "Select a value",
}

All data needed to create the slider remains directly in the annotation. The metadata itself can depend on variables and indeed even on other parametric values; the only thing Curv needs to be able to do at compile time is determine that the binding is parametric, which it can do by detecting the slider keyword. Its arguments can still be evaluated at runtime when the slider widget is actually created.

bluecube commented 5 years ago

I like @p-e-w 's idea with annotations, but I think having separate annotations for each parameter type is a bit too much.

I would propose annotation mut for mutable values and param for parametric ones. param could then create an implicit lambda from the annotation and store it inside the compiler for gui updates.

let
  param a :: slider(0, 1) = 0.5;
  param b :: slider(0, a) = 0;
in
  something(a, b)

btw, what happens if I use the parametric annotation in a non-parametric context (eg. let a :: slider(0, 1) = 1; in something(a))?

sebastien commented 5 years ago

@p-e-w to answer you question, I would see slider as a predicate with side-effects: it always returns true, but as a side-effect it attaches meta-information to the slot, which can then be exported by the compile for the GUI.

The mut and param annotations would definitely be cleaner than let_{mutable,parametric} and also have the advantage of not being expressions, so they are unambiguous -- but they would be redundant with corresponding predicates (let a :: mutable = ... ; b :: slider) provided the compiler is able to interpret these predicates at compile-time (Zig and I think D have some rich compile-time evaluation features, I think C/C++ also have that in recent versions).

In terms of language features, the most important thing to me is to have a way to add meta-information to the program that the compiler can later extract. For instance, you could have inline documentation with something like that:

let my_function (a :: is_num >> doc "First argument", b :: is_num >> doc "Second argument" ) :: doc "Returns `a` + `b`

This is probably not valid Curv code, but it gives an idea of how slot predicates could be leveraged to pass many types of useful meta-data. The alternative is finding another text-based encoding nested within the comments and write another parser for that -- it's what most languages do, but it implies the creation of supporting tools with duplicated functionality.

A GUI might want to retrieve information from the program and the std library to present it to the user (function prototypes, documentation, etc), and this would allow for an extensible encoding of meta-data in the program model itself. Then the communication of this meta-data is trivial using the compiler and the JSON-API protocol.

doug-moen commented 5 years ago

@p-e-w said: "One thing I'm confused about is that several of the above posts refer to a picker as a "predicate". I do not see how this is the case, or why it should be."

It might be a mistake, but slider(1,5) is a predicate function. slider(1,5) 1 is true, slider(1,5) "abc" is false. I did this so I could reuse the :: syntax to add picker annotations to variable names. I couldn't think of an alternate syntax that I liked better.

I do need to infer a type for shape parameters when I compile a parametric shape to GLSL, which is a statically typed language. The predicate part of a picker value checks that the value belongs to the correct type. If you initialize a shape parameter with a value that conflicts with the type of the picker, you will get a compile time error, because the predicate will return false.

The language requires that the RHS of the :: operator is a predicate value.

doug-moen commented 5 years ago

I need to do a better job of explaining the rationale, and the use cases that I want to design for. Also, there is a modified proposal for mutable variables at the end of this post.

First of all, Curv has two classes of users. "Designers" use the high level API to create art. "Developers" use the low level API to define new high level building blocks by implementing new distance functions and colour functions with low level coding.

Right now, everybody who is involved with the Curv project is a developer. There is a lot of work needed to improve usability before I feel ready to teach Curv in a classroom or hold a workshop for artists.

Part of the story on usability for designers is in the syntax. There is a tradeoff between making Curv look like a general purpose programming language (for familiarity to developers) vs making it as simple as possible for designers.

The High Level Interface

Unlike GP languages, a Curv program is an expression. A typical first program in Curv is a simple expression like cube. There is no extraneous syntactic cruft, as found in GP languages. You can do a lot with just simple shape expressions, so the first lesson in a Curv tutorial for beginners will begin with this.

In the second lesson, we can introduce parametric design using sliders. The current syntax looks like this:

   parametric {
      Size :: slider(1,5) = 3;
   }
   cube Size

This looks a bit alien compared to other Curv syntax, so I proposed this instead:

   let_parametric
      Size :: slider(1,5) = 3;
   in
   cube Size

Either way, the shape parameters are at the top of the program, and separated from the shape expression that follows. You can add a parameter clause to the top of a program without modifying the shape expression. Those are my basic goals for the syntax.

In the third lesson, we can introduce auxiliary definitions. The syntax for this is

   let
      <data definitions and function definitions>
   in
   <expression>

You can add a let clause to the beginning of a program without modifying the expression that follows (eg, you don't need to add a closing parenthesis or end keyword after the ). The order in which definitions are written within a let clause doesn't matter. This means you can use a top-down coding style, if you like, where the most important definitions come first, and utility functions are written afterwards. You can also define recursive and mutually recursive functions. This wouldn't be possible if we used "sequential scoping", and enforced definition before use.

If you want both shape parameters (with sliders) and auxiliary definitions, then you just compose let_parametric with let, like this:

   let_parametric
      Diameter :: slider(.5, 2) = 1;
      Length :: slider(2, 8) = 4;
      Colour :: colour_picker = red;
   in let
      candy = sphere Diameter >> colour Colour;
      stick = cylinder {h: Length, d: Diameter/8} >> move(0,0,-Length/2);
   in
   union(candy, stick)

It is bad for usability to combine let_parametric and let into a single construct. These two operators have very different semantics. Most important, they use different scoping rules. let creates a mutually recursive scope, where all of the definitions can see all of the bindings. The order of definitions doesn't matter. let_parametric uses different scoping rules: parameter definitions cannot refer to one another. The order of definitions is significant, it controls the order of value picker widgets in the parameter window.

Mixing parameter definitions and auxiliary definitions in a single list of definitions would be very confusing, because of the conflicting scoping rules, and the conflict between order dependence and order independence.

The Low Level Interface

When you write your own distance function inside of make_shape, you are using the low level interface. The target audience for this API is developers. The usability requirements are a bit different. Now we are dealing with experienced programmers.

Mutable variables are part of the low level interface. When I first implemented them, I thought it was a temporary kludge that would be removed once I had implemented tail recursion optimization for distance functions (so that distance functions can be programmed in a purely functional style).

I would still like to implement tail recursion optimization, but I no longer want to get rid of mutable variables. I port a lot of distance functions from other languages (mostly GLSL) to Curv, and rewriting other people's code in a functional style, using tail recursion for loops, seems like too much trouble. However, I do want to keep mutable variables segregated from the high level interface.

I proposed let_mutable as a replacement for the current syntax using do and var because it is more consistent with the high level interface for defining shapes (let_parametric and let). This leads to functions that look like this:

fbm xy =
    let shift = [100,100];
        rot = cis(0.5);   // Rotate to reduce axial bias
    in let_mutable
        var st := xy;
        var v := 0;
        var a := 0.5;
    in do
        for (i in 1..5) (
            v := v + a * noise st;
            st := cmul(rot, st) * 2 + shift;
            a := a * 0.5;
        );
    in v;

Here we are chaining together a let clause, a let_mutable clause, and a do clause. I think it is pretty, but it seems that everybody else who has commented, disagrees. People are claiming that this is "not usable" (by developers), and I assume that the problem is: it's not familiar, because few programming languages have syntax that looks like this.

The syntax does have precedents. The let ... in ... syntax is used by Elm, Haskell, O'Caml, and related functional programming languages. O'Caml has both let and let rec. Scheme has 3 binding constructs called let, letrec and let*, with different scoping rules. let has the same scoping as let_parametric in Curv; letrec has the same scoping as let, and let* has the same scoping as let_mutable. They can be nested, but unlike in Curv, parentheses pile up at the end of the program when you do this.

But most programming languages have a block syntax, where you can write a list of statements, consisting of intermixed actions, immutable variable definitions, and mutable variable definitions. In every language that has this kind of block syntax, there is a single consistent scoping rule: sequential scoping. The scope of a variable definition begins at the following statement, regardless of whether it is mutable or immutable.

Based on the feedback so far, I think that the current developer community would be more comfortable with a syntax that looks more like block syntax.

We can extend the do syntax to support a mix of mutable and immutable variable definitions, with consistent sequential scoping. Then the fbm function looks like this:

fbm xy =
    do
        shift = [100,100];
        rot = cis(0.5);   // Rotate to reduce axial bias
        var st := xy;
        var v := 0;
        var a := 0.5;
        for (i in 1..5) (
            v := v + a * noise st;
            st := cmul(rot, st) * 2 + shift;
            a := a * 0.5;
        );
    in v;

This is closer to block syntax than @p-e-w's proposal, because there is no more chaining of let and do. The do operator is part of the low level interface, and will be documented in the "Advanced Features" section of the manual.

One of my concerns in introducing let_mutable was to fix a design hole in the language. You can't use while loops in a list or record comprehension. This is not an important feature. My concern is to satisfy the design principle of composability: every language feature should be composable with every other language feature with no arbitrary restrictions. But, I can still accomplish this by using do as a replacement for let_mutable, since either keyword will work for introducing mutable variables into a local scope:

[ do var i := 0; in while (i < 10) (i; i:=i+1;) ]
// => [0,1,2,3,4,5,6,7,8,9]

This fixes the composability problem and is easy to implement.

doug-moen commented 5 years ago

A small update to my revised proposal: use mut instead of var:

fbm xy =
    do
        shift = [100,100];
        rot = cis(0.5);   // Rotate to reduce axial bias
        mut st := xy;
        mut v := 0;
        mut a := 0.5;
        for (i in 1..5) (
            v := v + a * noise st;
            st := cmul(rot, st) * 2 + shift;
            a := a * 0.5;
        );
    in v;

Rationale: the feature is called "mutable variables". Rust uses mut, F# uses mutable for mutable variable definitions.

sebastien commented 5 years ago

It seems like the consensus is that let_mutable and let_parametric feel awkward, syntactically. As @p-e-w puts it "They are long and unwieldy, and AFAICT would be the only language keywords containing an underscore."

Now, @doug-moen raised a good point: "It is bad for usability to combine let_parametric and let into a single construct. These two operators have very different semantics.".

If let_parametric is only there to define/declare paremeters for the sketch, then why not just use parameters or params. It gets rid of the underscore, denotes a semantic difference, and avoids confusion with the let.

The do solution instead of let_mutable looks better, although I would personally be glad to have imperative constructs out of curv — but it's nice to see that we can still have them if we really, really need them.

In terms of designer usability, I just asked the designers at the office and they said that "var" means "variable" and "mut" means "mutation" to them. Chloé said that "mutation" has the connotation of being involuntary as opposed to "variable". Personally, I think let for immutable and var for mutable is perfectly fine and understandable. Swift uses let and var and is among the best designed languages (syntactically) I've seen (with F# ;).

doug-moen commented 5 years ago

@sebastien said "In terms of designer usability, I just asked the designers at the office and they said that "var" means "variable" and "mut" means "mutation" to them. Chloé said that "mutation" has the connotation of being involuntary as opposed to "variable". Personally, I think let for immutable and var for mutable is perfectly fine and understandable. Swift uses let and var and is among the best designed languages (syntactically) I've seen (with F# ;)."

It's all a matter of having internally consistent documentation, and having unambiguous terminology.

In Curv, a constant is an expression whose value is known statically, without having to run the program. 42 is a constant. 2+2 is a constant. Given the definition pi=3.1416, then pi is a constant.

A variable is a name that is bound to a value. In mathematics, and in pure functional programming languages like Haskell, variables are immutable, which means that the value bound to the variable doesn't change once the variable becomes defined in its scope. However, if a function F defines a local variable V, then different calls to F (with different argument values) can cause V to have different values at different calls. Variables can also be constants (eg, pi).

Curv is a pure functional language, and variables are normally immutable. However, as a special case, you can use the do construct and the mut keyword to define mutable variables, that can be reassigned using the := operator.

p-e-w commented 5 years ago

@doug-moen

It is bad for usability to combine let_parametric and let into a single construct.

Not only do I disagree with this, I believe that the reverse is true: It is bad for usability to force users to place standard and parametric variables into separate blocks.

One important use case for parametric values is experimentally finding a value that will later become a constant. Thus I might want to temporarily make a value parametric, play around with it in the GUI, then make it non-parametric again and replace it with the "optimal" value found by varying the slider. With a unified let block, this is as simple as adding/removing a slider annotation. With separate blocks, a let_parametric construct must first be created, then the declaration moved into it, then the slider annotation added, then the whole process reversed to fix the value again.

Note that even with a unified let syntax, you can still have multiple nested let blocks. Thus if you want to group all parametric declarations at the start of a program like in your example, this remains possible with essentially the same syntax. Unifying let and let_parametric gives you strictly more options than separating them, so I find it hard to see how this could be anything other than a usability gain.

Your point about let and let_parametric having different semantics is of course correct, but mixed declarations can still be unambiguously resolved as shown in my above "rewrite rule" comment. The two are indeed different, but accounting for that difference is a task that I would rather not have to take care of manually.

Here we are chaining together a let clause, a let_mutable clause, and a do clause. I think it is pretty, but it seems that everybody else who has commented, disagrees. People are claiming that this is "not usable" (by developers), and I assume that the problem is: it's not familiar, because few programming languages have syntax that looks like this.

The fact that most programming languages don't look like this is certainly part of the problem (and a valid complaint, since familiarity is a huge advantage and most programmers don't come from OCaml or Haskell) but there is a more fundamental issue: My mental model of variables simply doesn't think of mutability as the most important grouping trait that bindings can have.

I might choose to order declarations alphabetically, by length, by type, or by all kinds of semantic considerations related to the rest of the code. But having a separate let_mutable block forces me to first sort by whether variables are mutable or not, and only afterwards by any other properties. And in order to change the mutability of a variable, I have to move it to another place in the code.

However, as a special case, you can use the do construct and the mut keyword to define mutable variables, that can be reassigned using the := operator. (emphasis added)

The := operator should really just be = IMO, including inside mut declarations. The overwhelming majority of programming languages doesn't distinguish between initialization and reassignment operators. When there is already the mut keyword to mark a binding as mutable, it seems unnecessary to additionally have a special assignment operator for mutable variables.

Of all the different syntaxes proposed in the above comments, I like @bluecube's proposal the most. It has the best of all worlds:

@sebastien

In terms of language features, the most important thing to me is to have a way to add meta-information to the program that the compiler can later extract. For instance, you could have inline documentation with something like that:

let my_function (a :: is_num >> doc "First argument", b :: is_num >> doc "Second argument" ) :: doc "Returns `a` + `b`

I agree that it would be good to have the ability to add such metadata in order to drive tools like documentation generators. However, I don't think this should be done using standard Curv function syntax (>>) because this makes metadata and functional code hard to separate, which is the opposite of what we want.

Taking a clue from other languages, something like this might be preferable (note the triple ///, indicating a doc comment):

/// Returns `a` + `b`
/// @arg(a) First argument
/// @arg(b) Second argument
let my_function (a :: is_num, b :: is_num)

This would be valid Curv code even today, and would be easy to extract with or without help from the compiler.

doug-moen commented 5 years ago

We do need a way to add documentation metadata to function parameters and record fields. It's needed for the IDE. However, this would best be discussed in a separate RFC.

doug-moen commented 5 years ago

@p-e-w:

I am now thinking that including the word "let" in "let_parametric" is a mistake. It's misleading, because this operator is primarily a constructor for parametric shapes (or more generally, parametric records). It also just happens to be a binding construct.

It's analogous to the -> operator, which constructs anonymous functions, and is also a binding construct.

x -> x + 1

constructs an anonymous function, and it also binds x as a local variable within the expression x + 1.

The so-called let_parametric operator is doing the same thing. It is a constructor for parametric records, which also happens to be a binding construct. The internal implementation is even similar, because let_parametric <parameters> in <body> works by constructing an anonymous function {<parameters>} -> <body>. This function is effectively called to construct a new shape every time you tweak a slider in the Viewer window.

Perhaps make_parametric would be a less misleading name. Let's see what that looks like:

   make_parametric
      Diameter :: slider(.5, 2) = 1;
      Length :: slider(2, 8) = 4;
      Colour :: colour_picker = red;
   in let
      candy = sphere Diameter >> colour Colour;
      stick = cylinder {h: Length, d: Diameter/8} >> move(0,0,-Length/2);
   in
   union(candy, stick)
doug-moen commented 5 years ago

In a call to make_parametric, the parameters comprise the interface to the parametric shape that you are constructing, just as in a function definition, the function parameters comprise the interface to the function you are are defining. So you want the parameters of a parametric shape to be defined in one place, not interleaved with local variable definitions, for the same reason that you want the parameters of a function to be defined in one place, not interleaved with local variable definitions.

I haven't talked about the interface to a parametric shape, because this part of the design is still in flux.

doug-moen commented 5 years ago

@p-e-w said: "One important use case for parametric values is experimentally finding a value that will later become a constant. Thus I might want to temporarily make a value parametric, play around with it in the GUI, then make it non-parametric again and replace it with the "optimal" value found by varying the slider. With a unified let block, this is as simple as adding/removing a slider annotation. With separate blocks, a let_parametric construct must first be created, then the declaration moved into it, then the slider annotation added, then the whole process reversed to fix the value again."

That is temporarily a valid argument. However, what I hope to see in the new IDE is the ability to click on a constant in the editor and "scrub" the value: a slider, or some other widget appropriate to the type of the constant, will pop up in the editor window and let you tweak the value. The shape will animate in the viewer window as you do this. And this IDE is under construction right now.

The purpose of make_parametric is different from this. It lets you design an interactive GUI interface for customizing a shape, which other people can then use, without necessarily reading or understanding your code.

Have you seen the Customizer feature on thingiverse.com? Some of the models have an "Open in Customizer" button, which lets you enter parameter values for customizing the shape. This is implemented by executing an OpenSCAD script that has magic, structured comments which specify what the GUI looks like. On the Thingiverse web site, the user doesn't see the OpenSCAD code while they are customizing a shape. I'm trying to create an equivalent feature for Curv, except properly designed, with parametric shapes as first class values. Syntactically separating the interface of a parametric shape from the code that implements it is part of the design.

doug-moen commented 5 years ago

Thanks for all of the feedback about let_mutable. It was a bad idea, and I'm glad you saved me from implementing it.

Starting from the feedback, I brainstormed several alternatives, then conducted "usability testing" by writing code using the new syntaxes. I narrowed it down to one candidate, implemented it in a branch, then iteratively modified the design further based on what I discovered during implementation.

What came out is very simple. The var x := 0 syntax is no longer required. Local variables are defined using let, where or for. All local variables have the same status, in other words. The := operator (assignment statement) may be used to modify a local variable. And that's it. There is no mut qualifier for declaring mutable variables, because the extra language complexity is not worth it.

The new version of Curv is so far backward compatible, but I'll want to deprecate the var syntax for defining local variables.

Rationale

Why no mut keyword? When I came up with let_mutable, I had been studying Rust, and in Rust, the distinction between mutable and immutable variables is critically important. Immutable objects can be safely shared between threads, mutable objects cannot, and so on. But Curv is not Rust, I now realize. Curv does not have the concept of shared mutable state, and it doesn't even have objects, only values. In imperative languages that are designed for large scale software engineering, it's very important to be able to control which of your shared objects are mutable, and which are immutable, but this is not a concern for Curv programmers. So there is a lot less value in being able to mark variables as mutable or immutable.

The other issue is simplicity. The typical Curv user will be a 3D printing enthusiast, or a designer or artist, who has some experience writing code, but we don't require users to be expert programmers. In Python, and some other popular languages used by beginners, there is no concept of marking local variables as mutable or immutable. Introducing this concept into Curv would add extra complexity that can't be justified in a language that is much simpler than Python.

Why is := used as the assignment operator? Because the = operator is already being used for variable definitions and function definitions and field definitions in records. When you see a = expr or f x = expr, it is part of a group of definitions that comprise a recursive scope, where the order of definitions does not matter. By contrast, x := 0 is an assignment statement, and you need to look backward to see where x is defined using x = .... Statements are executed sequentially: order matters. I do not want to use the same symbol for both operations, because it reduces the clarity of code.

doug-moen commented 5 years ago

This is done. The original RFC proposal was modified/abandoned, and the replacement design is implemented.

p-e-w commented 5 years ago

@doug-moen Sorry for not responding sooner, only now did I have a chance to look at this again.

I really like the final design you have arrived at. The one change I would propose is to rename make_parametric to simply parametric. It looks cleaner and more concise, and avoids having a single language keyword containing an underscore. Also, "make" sounds imperative, while Curv is purely functional, so this prefix doesn't really fit anyway IMO.

sebastien commented 5 years ago

I kind of agree with you, but at the same time it's consistent with make_shape, but the latter is not a keyword. parametric definitely looks cleaner to me.

doug-moen commented 5 years ago

I agree that parametric looks cleaner. I am considering this change.

In Curv, I have a small number of make_foo functions for constructing values of type 'foo'. The full set is:

I use shape as the name of a function parameter in many places, so I used make_shape as the low-level shape constructor to avoid a conflict. Then make_texture and make_blend followed.

I wouldn't call this a consistent naming scheme, though. E.g., for colour values, there are a bunch of constructors called red, green, blue, sRGB(r,g,b), and so on.

I didn't use make_colour because I need to support multiple colour spaces, not just the sRGB colour space. E.g., some people have laptops or monitors that support gamuts larger than sRGB.