Filament 0.3 Release
Fundamental Language Changes
I’ve been working a lot on new graphics apis, and new examples to exercise those apis. Things like turtle graphics, which are great for learning recursive functions, and image pixel processing, which are super fun and closer to Raytracing and GPU shaders than you might realize. However, along the way, I’ve discovered some missing features that have forced me to make some tough decisions. Today let’s talk about conditionals, jumps, and lambdas.
Small, Regular, and Clear
The goal for Filament has always been a language easy enough for kids to learn, and powerful enough for domain experts to use. It is not for producing reusable software artifacts. This goal has greatly shaped the language; encouraging a syntax that is small, regular, and clear.
It’s worth repeating this point: small, regular and clear. Small means there are fewer things to learn. Regular means different features work the same way. Clear means someone who has never seen the language could get the general idea of a code snippet from reading it. That’s why I use and
and or
instead of &&
and ||
for boolean operations and !
for factorial instead of not.
Small regular and clear is a worthy goal, but it is always in tension with power. You can get a pretty long way with eagerly evaluated pure functions but eventually you need more; like conditions. Can you believe Filament hasn’t had an if
statement until now!? Can you believe it’s a mostly functional language with no anonymous functions? These are critical features that have to exist eventually but they also complicate the language and fight against small, regular, and clear. So let’s dive in.
Conditionals
Conditionals are things which let you choose one code path or another. They are also called branch statements. In most languages this includes the if
statement, usually with an else
option, and switch
for multiple conditions in a row. Most C derived languages also include the ternary operator ?
which acts as a mini if
else
statement.
Object Oriented languages (and some functional languages) can also use multiple dispatch as a way to branch. This means you call the bar()
method on a Foo
object, but there might be different versions of bar()
that do different things. The language decides at runtime which version to call using either type signatures or the actual identity of foo
. This can act as a form of conditional. Filament doesn't have objects or type signatures, so I'll ignore multiple dispatch for now.
So far Filament hasn’t had an if
statement or any other conditionals because I haven’t built any examples big enough to need it. Of course this only worked because all of the standard library functions are implemented in Javascript, a language that does have conditionals. Now Filament needs its own.
What should the syntax look like? Let’s start with the C derived syntax for if
in Javascript.
if(x<0) { // the test
// do something
}
Javascript uses the keyword if
followed by an expression which resolves to a boolean (true or false) inside of parenthesis. This is often called the test of the conditional. It is followed by a code block in curly braces. If you want an else condition you can add it with the else
keyword and another block.
if(x < 0) {
// do something
} else {
// do something else
}
A lot of people don’t like the braces, but I’ve already decided code blocks are delineated with braces, so I should be consistent. Always Be Regular. However, it’s always seemed strange to me that C-based languages use if
and else
as keywords, but not then
. I think then
makes the code more readable when read by novices, plus it makes it easier to get rid of the parenthesis around the expression, if desired. I think it looks clearer, but I recognize this is just a personal preference. I could be swayed otherwise.
So now we have
if x < 0 then {
// do something
} else {
// do something
}
Remember that Filament doesn’t care about whitespace. This opens the possibility of short if
statements if we let the block be an expression instead.
if x < 0 then 5 else 6
This gives us a very small and clear syntax. I think it’s much better than the ternary operator in JS, which I find quickly turns to gibberish.
x<0?5:6 // when x is less than zero we.. um.. uh.. What now?
As it happens, in Filament code blocks are expressions. They simply return the result of the last expression in the block, so it makes complete sense to drop the braces for a single expression branch. Using keywords for if then else
keeps it all nice and clear. The if
statement itself is also an expression, so you can easily store the result of the condition in a variable, do more math with it, or return to the calling function.
4 + if(x<5) then x else 5
or
def doit(x) {
if(x < 5) then {
0
} else {
x*2
}
}
range(10) >> map(with:doit)
So I’m pretty happy with how if
statements turned out. I’ve ignored the else if / elif
case common in languages that need to handle more than two branches. I suspect that pattern matching will solve that case better, so I’m saving it for later. Now on to lambdas.
Lambdas
A lot of languages that purport to be "easy to learn" leave out lambdas, then end up adding them later. Java did this. So did Python. C# sort of started with partial lambdas, then had to add the real thing later. I don’t think we can avoid lambdas, but I’ve resisted adding them because they do seem to be hard to understand. What to do?
I’ve decided that the only way to handle a hard concept is to actually just teach the hard concept. Any ways of hiding it or coming up with alternatives is doomed to fail, because lambdas really are an essential feature of a modern language. So I’m just going to have to teach it gently and carefully. Let’s start with what a lambda is.
Virtually all programming languages have functions. It’s a subroutine with a name and some parameters. You call the function by name, give it your parameters, and it gives you an answer back. Depending on what else the function does it might be called a method or a procedure or something else; but it’s still a kind of function.
Functions are an easy concept. Even Scratch has them. You call them to do everything. Later versions of Scratch added user defined functions, which I feel is a logical progression. Use existing functions for a while and then make your own. (Though in Scratch they can’t return values for some reason).
Some functions take another function as an argument. You can send a function into another one. This is called higher ordered functions. It is a confusing concept at first, but I think they become more understandable through experience. If we start small with things like mapping a list then it becomes tractable.
list << [1,2,3]
def double (x) { x*2 }
map(list,with:double)
def square (x) { x*x }
map(list,with:square)
In the code above we see a custom function called double
which doubles any value sent into it. If you want to double everything in a list, map the list with the double
function. If you want to square every value in the list, make a square
function. square
and double
are these sort of mini functions that you can use in different ways.
Once you see functions used as arguments like this, it’s not a stretch to wonder why you need to need to name them at all. If you are only going to use a mini function in one place, and if it only has one expression inside it, then surely there’s a more compact way to write them. Congratulations! We’ve just discovered lambdas.
list << [1,2,3]
map(list, with: (x)->{x*2}) // double
map(list, with: (x)->{x*x}) // square
Lambdas are a set of arguments in parentheses and a code block, combined with some unique symbol. Many recent languages use some sort of arrow and I think that’s consistent with the rest of Filament. I did worry if it would collide with the <<
>>
syntax for pipelines and assignment, but I think they are different enough that people won’t be confused. I chose the single arrow of ->
instead of the double arrow =>
for stylistic reasons, but I could be convinced otherwise.
For the syntax I’ve tried to use the same progressive enhancement style shown in the if
statements. If you have just a single expression in the body then you don’t need the braces. Expression instead of Block. Further more, if you have only one argument, you don’t need the parens. The simple case becomes this:
list << [1,2,3]
map(list, with: x->x*2) // double
map(list, with: x->x*x) // square
Lambdas are also wonderful for plotting equations
// draw a quadratic
plot(y: x-> x**2 + 3*x + 4)
Since the arrow symbol is only used for lambdas in Filament this form won’t cause any ambiguity in the grammar. It’s also close to the syntax from several other well known languages so I’m pretty happy with it.
Whither Named Functions?
This does introduce a new wrinkle, however. We now have two syntaxes for making functions. One for full sized named functions and one for mini lambda functions. But are they really different? Full sized ones can specify defaults or if an argument is required. In the future it might even have types. The lambdas don’t do those things, but maybe they could.
After tossing this back and forth for a while I decided I don't like having two kinds of function syntax. It’s just one more thing to learn. So I collapsed them together. Now all functions are lambdas, some just get names. And since we already have assignment, a way to put things into names, we can just reuse that. We already save numbers and lists in variables, so why not functions too?! I like the regularity of it all.
Now a named function can be declared like this:
red << [1,0,0]
blue << [0,0,1]
make_color << (x:?,y:?) {
if x<5 then red else blue
}
Or used directly as a lambda
make_image( with: (x:?,y:?) -> [1,0,0]) //make image with just red
And since the common case is that a parameter is required, you can leave out the defaults if you want and they will be assumed to be required.
make_image( with: (x,y) -> [1,0,0]) // make image with red
Loops and Early Return
Lambdas plus conditionals make a language very powerful. We still don’t have regular loops, but with built in functions like range
and map
we don’t have as strong a need for loops as you might assume. We will still need them eventually, but they can wait until later.
Along with loops I’m going to have to answer another tough question: should Filament have early return? Meaning, should things like this be allowed:
() -> {
if x<5 then {
return 5
}
return 6
}
or
while(true) {
if x < 5 then break
}
In both of these cases the current execution jumps to somewhere else. Every language has something like this with return
, break
, continue
, goto
, or even straight jump
s. Currently Filament has none of these and I’m not sure it needs to (Scratch doesn’t have them either). What do you think about early return?
Thank you for reading this far and go check out Filament 0.3 and use the notebook interface.
PS
Along with the above big changes, I’ve also realized lists were badly broken, as well as tons of problems with nested promises in the JS implementation. All have been fixed in 0.3. There’s also new image and turtle APIs hiding in the tests dir, but currently disabled. Look for them to appear in a future release. Also the GUI now has the start of a symbol explorer and there’s a print function to print to the console. Enjoy!
Posted February 21st, 2021
Tagged: filament hl programming