1. Monocurl Scripting Language

META 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.

META 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.

Accompanying Video

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.

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.

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.

Functor Modes

The most prominent example is for setting the color of meshes, but it occurs in other places too.

1.5 Conclusions

META 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.

Sections

1.1 The Basics1.2 Functions1.3 Functors1.4 Functor Modes1.5 Conclusions1.6 (Optional) Intricacies