LanguagesArchitecture

We're going to commit a cardinal sin today and talk about syntax design! 0

There are three main alternatives for variable declaration:

  • Specifying whether the variable can change, such as:
    • Javascript's let x = 4 vs. const x = 4
    • Rust's let x = 4 vs. let mut x = 4
    • Java's int x = 4 vs. final int x = 4
    • Swift's let x = 4 vs. var x = 4
  • Not specifying whether the variable can change, such as:
    • Old Javascript's var x = 4
    • Go's x := 4 (though it's not really a keyword per se)
  • Leaving off keywords completely, such as:
    • Python's x = 4

The previous version of Vale used the first option, let and let mut. 1

However, it recently changed to a fourth option, which was so nice that it became the default behavior in version 0.2.

Side Notes
(interesting tangential thoughts)
0

It's such a travesty to be talking about something as mundane as syntax during the week we're prototyping deterministic replayability!

1

Vale is a language that aims to be fast and memory-safe, while still being easy to learn. This syntax change helps with that!

The Fourth Option

Most languages can't just use x = 4, because that already meant something. That's the assignment statement, which modifies an existing variable that we already declared.

And alas, Python tried combining those two statements. It didn't go well. 2

However, there's another option here: let's change the assignment statement!

Instead of this:

func main() {
  let a = 3;
  let b = 3;
  let c = 3;
  let mut d = 3;
  d = 7;
  println(d);
}
stdout
7

We can have this:

vale
func main() {
a = 3;
b = 3;
c = 3;
d = 3;
set d = 7;
println(d);

}
stdout
7

In other words, x = 3 declares a variable, and set x = 42 assigns it.

It was odd at first, but after using it for a few weeks, we think this is a huge improvement.

Syntax design can be a tricky endeavor. When exploring new syntax, we had to suppress the knee-jerk unfamiliarity avoidance, and actually experiment. Without experimenting, it's easy to get stuck with what's familiar, even if there are better options.

2

This leads to problems; if you rename your x = 1 declaration to y = 1, but forget to modify x = 42 assignment below, you now accidentally have two variables!

Why We Like It

To our great surprise, we've found that our codebases have a lot more declarations than assignments, so it makes sense to require the extra keyword on assignments because they're rarer.

We sampled three Vale projects. One had 111 declarations, and only 35 assignments. That's only 21% assignments! The other two were even lower, at 20% and 6%.

This isn't just Vale either. A randomly chosen Rust library, Rocket, had about 8%. 3

When did this happen? We used to assign variables all the time!

3

This is approximate; I used let(\s+mut)? (4437 results) and ^\s*[\w\[\]\.]+\s*[\+\-\*\/]?=\s+ (351 results) and which may have missed some corners, such as lambdas and mutating call returned values.

Parsing with regular expressions is fun!

We suspect moving towards more declarative patterns has contributed to this shift. Let's see some examples!

First, we no longer need assignment to return a value from an if-statement. Compare these two snippets in Scala:

scala
var weight = 0
if (i_am_a_potato) {
  weight = 42
} else {
  weight = 73
}

scala
val weight =
  if (i_am_a_potato) {
    42
  } else {
    73
  }

Second, our for-loops became foreach loops, removing that pesky i++. Compare these two snippets in Java:

java
for (int i = 0; i < ships.size(); i++) {
  ships[i].launch();
}

java
for (Spaceship ship : ships) {
  ship.launch();
}

Third, we loop over collections less, and now use specialized methods like find a lot more. Compare these two snippets in Javascript:

js
let foundIndex = -1;
for (let i in ships) {
  if (ships[i].name == "Firefly") {
    foundIndex = i;
  }
}

js
let foundIndex =
    ships.findIndex(
        x => (x.name == "Firefly"));

Nevertheless, we seem to have a lot less assignments nowadays, so it makes sense to have the extra keyword on the rarer statement, not the more common one.

Another Factor

Note how the declaration doesn't specify whether we can change the variable, like let vs. let mut.

One of the benefits of that distinction was that we could easily know whether the variable could change in the future.

We actually kept that distinction for a while; we used the ! symbol, such as d! = 3.

However, we decided to not require it for local variables, because the set keyword makes assignment more noticeable than it was before.

func main() {
  a = 3;
  b = 3;
  c = 3;
  d! = 3;
  set d = 7;
  println(d);
}
stdout
7

For example, if we want to know whether the variable d = 3 can change, we just need to look for a set d = 7 keyword somewhere in the function, which is much more noticeable now than the previous assignment syntax was.

However, that reasoning doesn't apply to structs. A struct's members might be modified from various far-flung files in our codebase.

vale
struct MyStruct {
x! int;
}

For that reason, we kept the ! on struct members. Other languages do this as well, such as OCaml, and it seems to be a pretty good balance.

Conclusion

Of course, we can't generalize too much. Every language is different, so we can't say that every new language should use this new scheme. Still, newer languages should give it some thought!

Thanks for visiting, and we hope you enjoyed this article!

In the coming weeks, we'll be writing more about our "region borrow checker" which helps eliminate Vale's memory safety overhead, so subscribe to our RSS feed, twitter, or the r/Vale subreddit, and come hang out in the Vale discord!