Closed doug-moen closed 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.
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.
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.
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.
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.
@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:
let_mutable
→ let mut
or let _ :: mutable
let_parametric
→ let _ :: parametric
parametric(p)
and mutable(p)
to compose predicatesParametric 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.
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.
@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.
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.
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)
)?
@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.
@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.
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.
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 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.
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.
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.
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# ;).
@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.
@doug-moen
It is bad for usability to combine
let_parametric
andlet
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, alet_mutable
clause, and ado
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 themut
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:
let
block@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.
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.
@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)
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.
@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.
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.
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.
This is done. The original RFC proposal was modified/abandoned, and the replacement design is implemented.
let_parametric
is now called make_parametric
.let_mutable
has been withdrawn. The assignment operator (:=
) has been updated so that you can reassign any local variable defined using let
, where
or for
. The var name := value
definition syntax is still implemented for backwards compatibility, but it will be deprecated then removed in the future.@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.
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.
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.
These are the changes to Curv language syntax that I propose for release 0.4:
let_parametric
is a variant oflet
that defines variables which are shape parameters, and which can be associated with sliders and other graphical value pickers. It replaces theparametric
keyword in the prototype implementation of value pickers.let_mutable
is a variant oflet
that defines mutable variables. It replaces the undocumentedvar
keyword.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:
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 of3
.There are 3 associated language features:
::
, the predicate assertion operator.slider(1,5)
.let_parametric
.predicate assertions
An expression of the form
value :: predicate
asserts thatpredicate(value)
returnstrue
, and then it returnsvalue
.For example,
x :: is_num
returnsx
, after first checking thatx
is a number, and aborting the program ifx
is non-numeric. You could achieve the same thing by writingname :: predicate
is a predicate pattern that binds a value V toname
, after first checking thatpredicate(V)
is true. If the predicate is false, the pattern match fails.For example,
ensures that
n
is a number when it is defined.For another example, given
then
cuboid 3
is a cube of size 3, andcuboid[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:
checkbox
-- A boolean parameter (true or false), represented by a checkbox widget.colour_picker
-- An RGB colour value. The widget allows you to edit RGB or HSV colour components directly, or use a colour wheel to select colours visually.slider(low,high)
-- The parameter is a number in a continuous range betweenlow
andhigh
. A linear slider widget is used to set the number.int_slider(low,high)
-- The parameter is an integer betweenlow
andhigh
. A linear slider widget is used to set the integer.scale_picker
-- The parameter is a scale factor: a number > 0 and \< infinity. The widget lets you increase or decrease the value by dragging with the mouse, and the value changes according to a logarithmic (not linear) scale. This is the same logic used to modify the zoom factor in the Viewer window using a mouse scroll wheel or trackpad scroll gesture.let_parametric
To add graphical parameters to a shape in a Curv program, you prefix a shape expression with a
let_parametric
clause:This is an expression that returns a parametric shape value.
Each
<parameter>
has the form:A
let_parametric
expression differs from alet
expression in several ways:let
allows mutually recursive definitions, while the parameter definitions inlet_parametric
cannot reference one another.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 ado
block.The new syntax uses
let_mutable
to define mutable variables. With this change,while
loops within list comprehensions and record comprehensions.The ability to define mutable variables using
var
will be deprecated.Mutable variables in an expression:
Mutable variables in a list comprehension. In this case, we don't use
do
: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.