This is part seven in a series on the 0.3 version of the language spec for the Merg-E Domain Specific Language for the InnuenDo Web 3.0 stack. I'll add more parts to the below list as the spec progresses:
In this post we are going to look deeper into the subject of freezing. While the idea of freezing mutable data in most languages that implement the subject is quite simple from a user perspective, in Merg-E, partially to keep the language itself simple, and partially for least authority reasons, it's a bit more involved. That is, you will be exposed a bit more to what freezing actually does and you will need to actually think about it quite a bit more.
The most straight forward use of freeze without any modifiers is the freezing of a scalar of a simple type like int64, bytes, or string. One thing we need to realize is that while from a least authority point of view, even while immutable data in general is considered better than mutable data and mutable data, especially shared mutable data should be minimized, the act of making mutable data immutable in the Merg-E philosophy is one that carries authority on its own. As such it was decided that the freezability of a singular variable depends on modifiers of the variable definition:
| expression | freezable |
|---|---|
| int max_prime = 100 | YES, already immutable |
| freezable int max_prime2 = 100 | YES silently discarded, already immutable |
| mutable int counter = 0 | YES, not shared or borrowed |
| freezable mutable int counter2 = 0 | YES, silently discarded, not shared or borrowed |
| borrowed mutable int ok_count = 0 | NO |
| freezable borrowed mutable int ok_count2 = 0 | YES |
| shared mutable int ok_count3 = 0 | NO |
| freezable shared mutable int ok_count4 = 0 | YES |
Now if we invoke:
freeze ok_count4;
The shared mutable will be turned immutable for the rest of it's lifetime.
You might have thought that is it when it comes to shared mutable scalars, but the fact that we may want to give one function the ability to freeze its function argument or explicitly closure captured shared scalar doesn't mean we want every function we share it with to have the authority to freeze. So even if the variable is already freezable, we still need to make this explicit, or the compiler will implicitly pass a scalar protected by a non-freezable proxy.
all_primes += is_prime(counter, no_proxy<unfreezable> ok_count2);
Note that we use the parameterized modifier no_proxy to disable the automatic insertion of an unfreezable proxy. Parameterized modifiers are still a very fluid part of the language specification, so this part of the syntax may still change, but for now this is the syntax.
The same construct is available for explicit closure captures.
In the previous post we already saw dataframes getting frozen. It is important to note that dataframes are special in this regard. A dataframe on creation is empty and mutable. While it's being constructed, rows of immutable scalars are added to it. But before you can actually use the dataframe for anything, and I mean anything, including assignment, using it as function argument, or use it in an inline expression, the dataframe needs to get frozen. Not doing so will result in a compile or runtime error depending on the runtime. Basically a non-frozen dataframe is unusable like a house that is still under construction.
As we discussed in the previous post, DAGs can be used as poor man's structs or dictionaries/maps. If you are weird and stuck in OO paradigms, you could even use a simple DAG to mimic an object, but that is considered non-idiomatic. But for the data-only DAGs, freezing becomes a bit more involved than for scalars.
As long as it is a data-only the expression :
freeze myDag;
and
deep freeze myDag;
mean exactly the same. Both freeze each node in the dag so that scalars are now immutable and non-leaf nodes can no longer be pruned or grafted and no new aliases can be created.
But there are other modifier too:
| modifier | root node | intermediate nodes | scalar leaf nodes |
|---|---|---|---|
| <none> | freeze | freeze | freeze |
| deep | freeze | freeze | freeze |
| leaf | - | - | freeze |
| shallow | freeze | - | freeze |
| noleaf | freeze | freeze | - |
It may seem that the deep modifier does nothing, but we will see that when we look at functions in the DAG things change.
As we have seen, functions can be defined as mutable because they either wield shared (ambient) authority, or because they share or borrow mutable state. When a function holds a shared or borrowed mutable state and the function is part of a DAG that the user tries to freeze, this mutable state should freeze too in order for the deep freeze to succeed. However doing so could very well break the function because the function expects the mutable state it feels it governs to remain mutable, leading to delayed runtime errors occurring.
The code has the ability to consult scope.self.frozen to see if it is supposed to be frozen now, but we can not expect every single function to be defensively written that way. Instead we define a modifier with what the function is marked as being defensively written that way:
freezable mutable function report (count int)::{
cout;
endl;
}{
cout "counted " ok_count " prime numbers" endl;
};
Basically when a freeze is done on this function (as part of a DAG freeze, freezing individual functions, while possible, is considered non-idiomatic), the runtime will look inside of the guts of the explicit capture of cout and endl and freeze them too. But for this example this isn't going to work like this because while endl is freezable, cout isn't, but cout is what is called nullable, so we need to make another change:
freezable mutable function report (count int)::{
freeze_replace<nullexpression> cout;
endl;
}{
cout "counted " ok_count " prime numbers" endl;
};
Again we resort to a parameterized modifier, in this case freeze_replace. What it does, combined with the nullexpression identifier, is that it explicitly tells the runtime not to try to freeze cout, but instead try to replace cout with its own defined nullexpression, but only within the function being frozen, not globally.
To illustrate, let's not rely on cout having a null expression. We can do this ourselves in our function itself.
freeze_replacable mutable function report (count int)::{
cout;
endl;
}{
mutable function __nullexpression__(count int){
}{
};
cout "counted " ok_count " prime numbers" endl;
};
This usage is advocated as the most idiomatic way to make functions play nice in a freezable DAG. The function isn't actually made freezable, it is made replaceable by its internal do nothing replacement.
Now that we know how functions can be frozen, we can look at the difference between freeze and deep freeze that so far looked like they were doing exactly the same. The difference is in what happens when you try to freeze a DAG that contains functions that are neither freezable nor freeze_replacable. A standard freeze will ignore functions that aren't either of these, while a deep freeze will make this result in a compile or runtime error depending on the runtime.
But what if we have a DAG that we don't want the invoked function to freeze any part of because allowing that would give away excess authority? There is an option for that from capability theory, that is a type of recursive wrapper around the DAGs root node called a membrane.
myblocker += my_function( membrane<nonfreezable> mydag );
With this expression we hand a the mydag DAG to my_function, but instead of handing it mydag itself, we wrap mydag in a non-freezable membrane instead so that every DAG traversal operation results in a sub-tree that itself is wrapped in a similar non-freezable membrane itself.
In this post we did a bit of a deep dive into the subject of freezing. We also touched on the still very fluid subject of parameterized modifiers that may or may not become part of the 0.3 language spec in this form. Further we caught a tiny glimpse on the subject of capability patterns by exposing ourselves to a snippet of the membrane pattern.
I'll need a few more posts to talk about things like attenuation in more detail, parallelism models, extend on capability patterns, and a few more.