1. Monocurl Scripting Language
We'll now talk about the Monocurl Scripting Language, which is the language used to create the instructions for all Monocurl programs. Prior programming experience is beneficial and this article is written with the assumption of background knowledge. However the language itself is quite simple and hopefully is easy to follow regardless.
Feel free to skim the basics section and come back to it whenever you have questions, but I definitely recommend you read the portions about functions and functors thoroughly.
1.1 The Basics
Monocurl comes as a pretty simple language. It is run line by line, and interpreted (so not very fast, but most of the heavy lifting is done in C). It is inspired by functional languages, though is still written somewhat imperatively.
We can declare a variable by saying var x = 0
or let x = 0
to denote it as either mutable or immutable, respectively. If a variable is mutable, we can later reassign to it using x = 1
, but cannot in the case of latter.
Monocurl is dynamically typed, which means that a given variable does not have a fixed type. Here is a list of the fundamental datatypes.
- double is simply a single scalar or numeric value. Note that there is no integer or boolean types, and you are expected to use doubles in their place. You can assign a double to a variable using a base-10 literal (e.g.
var x = 4.5
). - vector is a list of items (all subitems need not be of the same type). Example:
var x = {1,2,{3,4}}
. Notice how we can nest vectors (or anything really) within themselves. Use{}
for the empty vector. - character is just a datatype representing an ascii character (i.e. latin characters). Use
'char'
to initialize (e.g.'C'
or'%'
). - string is a shorthand for a list of characters, used very rarely. Example:
var x = "string_literal_example"
. Use%"
to insert a quote in the middle of a string. You can also do string interpolation via%{interpolated_expression_value}
, but this is also seldom used. - map is a mapping of keys to values, sometimes called a dictionary in different languages. Under the hood, maps are implemented as hashmaps. If you traverse a map, it is guaranteed it will be traversed in insertion order. Example:
var x = {0: 2, {0}: {0}, {}: 12}
. Use{:}
for the empty map. - mesh is a collection of points and vertices. We'll talk more about meshes in the future.
- animation is a special type of object used to modify state. We'll talk more about animations in the future.
- function is something that takes in a value and spits out another. We'll talk more about them in the next subsection
- functor is essentially the result of a function that remembers how it was created. We'll talk more about them in this article
The final point we note about language semantics is that Monocurl is entirely copy by value. For instance, consider the following example.
var x = {0,1,2}
var y = x
y[0] = 2
/* x is now {0, 1, 2} */
/* y is now {2, 1, 2} */
Monocurl supports basic control flow. if
, else if
, and else
statements are all supported in addition to while loops. For loops can be used to iterate across the elements of a vector
or the keys of a map
. You do not need to end the control statement with any special character (e.g. ;
or :
or {
). Here are a few examples.
let vec = {0,2,3,4}
var map = {2: vec, 1: "string"}
for x in vec
map[x] = x
var out = {}
for key in map
/* += is an append operation, see below */
out += key
/* map is now {2: 2, 1: "string", 0: 0, 3: 3, 4: 4} */
/* out is now {2, 1, 0, 3, 4} (due to the insertion order semantics) */
Here are the operators present in Monocurl.
+ - * / < > <= == != || && !
are all defined on scalars according to their usual meaning**
is the exponentiation operator according to its usual definition, where both sides are scalars.- Important + is also defined when the left hand side is a vector. In particular, it returns a new vector that is equal to the original vector, with the right hand side being appended. For instance,
{0,1,2} + 3
evalutes to{0,1,2,3}
, but also note that{0,1,2} + {3}
evalutes to{0,1,2,{3}}
. - You can index vectors or maps using
collection[index]
syntax. - You can create a vector of all integers on the interval [start, end) using
start :< end
, but I am thinking of removing this since it's ugly.
To illustrate some of the semantics of Monocurl, an example is provided below
var scalar = 0
var vector = {scalar, 2, {0: 4}}
scalar = 4
var vector_copy = vector
/* adds a single element (the 2-vector), instead of adding 2 elements */
/* note that 'vector' is not affected by this operation, only 'vector_copy' is */
vector_copy += {13, 14}
if vector[0] || vector[1] > 4
/* not executed, both conditions above are false */
for x in vector
vector_copy += x
else if !vector[0]
/* executed */
var s = 0
while s < 100
/* sum cubed due to order of operations */
s += s * s ** 2
/* s is now 1010 */
else
/* not executed */
vector_copy = vector
1.2 Functions
Monocurl takes significant inspiration from functional programming. Functions are useful because they provide a way apply transformations on meshes (among other reasons), leading to powerful results. If you're curious, we emphasize that (nearly) all functions are pure
IMPORTANT You can call declare and initialize a function in the following manner:
func function_name(arg0, arg1, arg2, ...) = expression
For a concrete example, here's an implementation of a two-dimensional determinant function
func det(a, b, c, d) = a * d - b * c
Which can be called rather easily in the following manner
let identity_det = det(1, 0, 0, 1)
There are a good amount of functions provided in the standard library, but to write a proper Monocurl program, you must not be scared of writing functions yourself. The syntax of function creation is purposefully very simple to facilitate their usage.
IMPORTANT The final point is about capturing variables. Suppose we create a function that references a local variable, then Monocurl requires that local variable to be immutable. Functions are considered immutable, so you can capture a function inside of a function!
let referenced_local = 3.1415926
let two = 2
func times_two(x) = two * x
func permiter(radius) = times_two(referenced_local) * radius
Capturing mutable variables is extremely confusing, and was thus outlawed. If you do have a mutable variable you want to capture, then just create an immutable copy. This makes the semantics much more clear.
var naively_mutable = 4
naively_mutable = 3
let capture = naively_mutable
func example_function(x) = x * capture
naively_mutable = 4
/* example_function(2) is 6 even though naively_mutable changed value */
/* this is because we captured the capture variable, */
/* whose value is always 3 */
We mention some final points about functions in the intricacies section if you are curious.
1.3 Functors
For better or for worse, functors serve a number of different usages and are one of Monocurl's unique features. We'll first give motivation for their existence.
Suppose Monocurl only had functions. It turns out that interpolation is rather annoying. We would likely have to do something along the lines of the following to move a circle to the right.
/* first parameter is center, second is radius */
let start = circle_creation_function({0,0,0}, 1)
let end = circle_creation_function({1,0,0}, 1)
REMARK Well actually, theres lots of different ways that can be done, but this is just a toy example to illustrate the point. Anyways, we can see theres a lot of repetition. This repetition gets worse the more parameters there are.
We want a way to say that end
should be exactly the same as start
, but the only difference is that the center
input of the function is changed. This is where functors come in.
IMPORTANT Functors remember the function that created them and the arguments that were passed in. We can then change any of the arguments, and the entire value is recalculated. The syntax is as follows:
let start = circle_creation_function:
center: {0,0,0}
radius: 1
var end = start
end.center = {1,0,0}
/* end is now a new circle */
Notice that functors are much easier to read than function invocations, and how we dont need to specify redundant arguments in the creation of end
. Moreover, we emphasize that you can create a functor from anything, even user created functions. For instance, we can do the following with the determinant function from above:
let org_det = det:
a: 1
b: 0
c: 0
d: 1
var new_det = org_det
new_det.a = 2
/* new_det how has a value of 2, but still remembers the arguments that created it */
new_det.d = 2
/* new_det now has a value of 4 (2 * 2 - 0) */
IMPORTANT You can think of a functor as "wrapping" a value. In many ways, new_det
acts like a number, and the only difference is that we have information about how it was created.
IMPORTANT Another important point of functors is that we can have sub expressions in the initialization period. Lets use our hyptothetical circle_creation_function
(shortened to circle now) to see what we mean by this:
var scene_variable = 0
let created_circle = circle:
let center_x = 2 - 4
let center_y = center_x - 1
let center_z = 4
center: {center_x, center_y, center_z}
/* we can even change variables not declared in the initialization scope */
scene_variable = 1
let r = 2 ** 4 - 1
radius: r
/* scene_variable is now 1, which might not be obvious! */
Again, this is a toy example and it would be easy to inline the values, but hopefully you see the benefit this can bring for more complicated examples.
And finally, this leads to the final use case of functors.
IMPORTANT Without functors, we can't create functions that need more than one sub expression of computation because you must write something along the lines of f(x) = sub_expression
. However, now consider what happens if we use the identity functor.
func determinant(a, b, c, d) = identity:
let diag_1 = a * d
let diag_2 = b * c
element: diag_1 + diag_2
Now the function technically returns a functor instead of a number, but the identity
function literally does nothing with the input by defintion, so it acts "invisible." Thus we are returning a number for most practical purposes.
It definitely takes some time to get used to this sort of syntax, and in the future I might add a better way to achieve the same. Currently however, this is the only real way to create functions that need multiple sub expressions.
We mention some final points about functors in the intricacies section if you are curious.
1.4 Functor Modes
For some functors, there may be default arguments that are normally hidden to prevent clutter. You can click the <>
to cycle through the different functor modes to reveal these.
The most prominent example is for setting the color of meshes, but it occurs in other places too.
1.5 Conclusions
If you honestly understand this article (especially the part about functors), then you have a good understanding of the Monocurl scripting language.
1.6 (Optional) Intricacies
I realize that functors is probably not the best name given that functors in the traditional Category-theory sense are different (and even different than functors in CS), but I chose this early on and have just stuck with it.
You can have functions return (useful, non-identity) functors, but this actually does get a bit confusing and we advise against it.
I actually think allowing all functions to be functors was a mistake and will probably separate them out some time in the future. The reason why I'm unsure about separating them fully is that it's still nice to call things that should just be functions using functor syntax, but at the same they shouldn't need to remember their arguments. Essentially, functors are too many things at once, which makes separating them difficult.
In cases where a function takes a function as a parameter, you MUST call it using functor syntax (again mentioning the same design flaw from above).
Because all functions are basically lambdas, there are certain types of recursion that Monocurl cannot adequeately simulate. You can do single function recursion just fine, and in fact you can do two functions calling each other (by declaring the second function in the initialization period of the other), but it can be shown its impossible to have three functions that can pairwise call each other. In practice, this doesn't turn out be that big of an issue, but is something to be aware of if the situation arises.