Chris Rybicki has recently added support for let var
to Wing (see
the pull request), and I thought it might be a good
opportunity to share our thoughts on the topic of immutability in Wing.
One of Wing's design goals is to help developers write safer code. Change in state is a major source
of complexity (and bugs) in software. Eric Elliott's Dao of Immutability describes it beautifully:
"The true constant is change. Mutation hides change. Hidden change manifests chaos. Therefore, the
wise embrace history"
A language-level guarantee that state cannot change offers opportunities for caching, runtime
optimizations and lock-free concurrency. Those attributes are very useful in distributed systems.
Immutable by default
This is why, similarly to other modern programming languages such as Rust and Go, we are designing
Wing to be immutable by default.
Let's look at an example:
let my_array = [1,2,3,4];
The above code defines an immutable array with the contents [1,2,3,4]
and assigns it to
my_array
. Immutability means that the contents of the object cannot be modified.
So if we try to add an item:
Eventually we would want this error to be something like Operation "push" is only available on
mutable arrays. Did you mean to declare the array with MutArray<num>?
, but bear with
us...
This is because the type of my_array
is Array<num>
, which represents an immutable array, it
simply doesn't have any methods that will cause it to change. In Wing, the following types are
immutable: str
, num
, bool
, Array<T>
, Set<T>
and Map<T>
.
If I wanted to define it as a mutable array, I will need to be explicit:
let my_mut_array = MutArray<str>["hello", "world"];
And now we can go wild:
my_mut_array.push("go wild!");
Similarly, we can define other mutable collection types:
let my_set = MutSet<str>{"hello", "world"};
let my_map = MutMap<bool>{"dog": true, "cat": false};
By the way: we are still debating if the standard types should be pascal-cased (e.g. Array<T>
,
MutArray<T>
) or snake (array<T>
, mut_array<T>
). Let us know what you think!
Yes! We are going to make this slightly harder to define mutable collections.
In the future, maybe we will introduce some syntactic sugar like:
This design concept is what's called "good cognitive friction" (or "mechanical sympathy"). It is
introduced intentionally in order to make sure the user understands the system better and encourage
best practices.
Reassignability
But immutability is not enough! Since we reference our array through my_array
, the compiler also
needs to guarantee that my_array
will always point to the same object.
Let's look at a hypothetic example:
let i = 10;
new cloud.Function(inflight () => { print(i); }) as "f1";
i = 20;
new cloud.Function(inflight () => { i = i + 9; }) as "f2";
i = i - 90;
What value will the cloud function print? We can't tell because i
is reassigned in multiple
locations and there is absolutely no way to determine its value.
This is where reassignability comes into play. In fact, in Wing, the above example would have
failed compilation:
OK, now we can relax. The Wing compiler tells us that i
is not reassignable.
Reassignability is a form of mutability (it is mutating the reference) and most modern programming
languages are trying to encourage single assignment. let
in Rust, :=
in Go, and const
everywhere in JavaScript.
So how do you make something reassignable? You can use let var
:
let var s = "hello";
s = "world";
You can also use var
in class and resource declarations:
class Foo {
i: num;
var s: str;
init() {
this.i = 10;
this.s = "world";
}
bar() {
this.s = "hello";
this.i = 20;
}
}
It can also be used in argument declarations:
let handler = inflight (var x: str) => {
if x == "hello" {
x = "${x} world";
}
};
Why let var
?
We originally considered using var
instead of let
var
, but we realized this is making it too easy to do the wrong thing. Entire code bases will be
written with just var
and mountains of linters will be written to protect you from shooting
yourself in the foot.
Going back to this concept of "good cognitive friction". If you need to type a few more characters in
order to make a variable reassignable (let var
versus let
), you will likely just use let
most
of the time, and the world will be a better place with less bugs and happier developers.
The Inflight Connection
So how is all this related to cloud development?
One of the very cool things about immutable state is that the compiler can create as many copies of
it as needed. If the compiler has a guarantee that a blob of data will never change over the
lifetime (and space) of the system, it can simply distribute it where it is needed.
This means, that in Wing, immutable data can be seamlessly referenced from any inflight
context.
Let's look at a very simple example just to explain the idea:
bring cloud;
let my_array = ["hello", "world"];
new cloud.Function(inflight (_: str) => {
assert(my_array.length == 2);
}) as "test";
So what's going on here? We have defined a cloud function that simply references my_array
. As much
as this looks simple and intuitive, the compiler actually had to do a bit of work to make this
happen. As a reminder, a cloud.Function
represents a cloud compute platform (such as AWS Lambda).
This means that the code inside the inflight
block is going to be executed sometime in the future,
on some other machine. Completely isolated from the original memory space in which my_array
was
defined.
Since our array is immutable, the compiler can safely clone it and bundle it together with the code
that runs inside the cloud function.
In the future, the compiler will be able to identify that my_array.length
itself is immutable,
and will only copy its value (see #1251).
If we try to reference a reassignable variable from inflight code:
let var s = "hello";
new cloud.Function(inflight (_: str) => {
print(s);
});
If we try to reference a mutable collection from inflight code:
bring cloud;
let my_array = MutArray<num>[1,2,3,4];
new cloud.Function(inflight (_: str) => {
assert(my_array.length == 4);
});
In this case as well, the compiler won't allow us to reference a mutable object within an inflight
context, because it won't be able to guarantee correctness.
Unsupported yet, but we will also have clone()
to cover you in case you want to reference a
snapshot of a mutable collection (clone_mut()
returns a mutable clone):
let mut_arr = MutArray<num>[1,2,3];
let arr = mut_arr.clone();
new cloud.Function(inflight () => {
assert(arr.length == 3);
});
See this pull request if you are curious how
immutable capturing works in Wing (for the time being).
What about user-defined types?
In the current revision of the language specification, we still haven't covered the idea of
immutable user-defined types (its on our roadmap).
This means that the compiler only allows capturing primitives, Array
, Map
, Set
, Json
(coming
soon) and structs (coming soon). Any other type cannot be captured directly. This means you will
likely need to extract any information from the object in order to reference it within an inflight
context.
Summary
There are endless ways to express ideas using code and we believe a programming language should be
designed to make it intuitive for developers to write better, safer and more robust code. We use
"good cognitive friction" such as let var
and MutXxx
to get our brain to spare another cognitive
cycle on choosing some programming approach.
Making Wing "immutable by default" is designed to encourage developers to write more functional and
immutable code. We continue to think of how to do it in elegant, simple, and not annoying ways, and
we would love your feedback and suggestions on Wing Slack.