@RalfJung
Great post as always. One thing that I found interesting was your discussion around different kinds of variance – one for owning and one for borrowing. I too have thought about that from time to time. Because &
freezes, it so happens that – in the absence of Cell – using the “sharing variance” for everything mostly works out, but when it doesn’t, it can be annoying.
One particularly annoying case I have found involves the use &mut
, which winds up forcing you to gradually accumulate more and more lifetime parameters. For example, in the compiler, we have this base context, the query context. Let’s say it’s type was this &'cx QueryContext<'qcx>
(the actual type is different, but this version is better for the purposes of this exposition). Here 'qcx
represents the lifetime of the global memory arena, and things tend to be invariant in it due to shared mutability and so forth. Note that this is a shared reference – this is because the qcx is used so omnipresently.
Now we want to make some analysis, and it needs to keep a reference to the tcx. So it looks like this:
struct TypeCheckContext<'cx, 'qcx> {
qcx: &'cx QueryContext<'qcx>,
}
Now, often, it turns out that the TypeCheckContext
might need to store some additional references. For example, maybe it accumulates a list of errors. These references can also be given the 'cx
lifetime. In effect, 'cx
becomes kind of the lifetime of the “type check context” (which is shorter than the lifetime of the QueryContext
as a whole):
struct TypeCheckContext<'cx, 'qcx> {
qcx: &'cx QueryContext<'qcx>,
errors: &'cx mut Vec<Error>,
}
This means I can create and use a type-check context like so, which is convenient:
fn type_check(qcx: &QueryContext<'_>) {
let mut errors = vec![];
let typeckcx = TypeCheckContext { qcx, errors: &mut errors };
...
}
Now maybe I wind up creating another layer of context, let’s call it the MethodCallContext
. Let’s say, moreover, that I am trying to pass the TypeCheckContext
using &mut
– this is likely true, because otherwise I wouldn’t be able to (say) push things onto the &mut Vec<Error>
contained within. If I try to use the same pattern, though, where I have the “named, invariant” lifetime 'tcx
and the “temporary lifetime” 'cx
, things go wrong:
struct MethodCallContext<'cx, 'qcx> {
typeck_context: &'cx mut TypeCheckContext<'cx, 'qcx>,
...
}
But here I run into trouble! The problem is that &mut T
is invariant with respect to T
, and hence 'cx
is invariant. Therefore, I can’t re-use it as the “lifetime of this inner context”. I need to make a separate parameter:
struct MethodCallContext<'cx, 'cx1, 'qcx> {
typeck_context: &'cx mut TypeCheckContext<'cx1, 'qcx>,
...
}
You start to see why this is annoying now. =) These can really build up.
So why is &mut T
invariant with respect to T
? It’s different from Cell<T>
– sort of the inverse. After all, you can mutate an &mut T
only when you have a unique path (and then, the alias we are concerned about is the original owner, who expects to recover the referent with its original type).
Sometimes I tinker with the idea of &uniq
references. These would be like &mut
but with a crucial difference – you cannot mutate the referent directly. Instead, you can declare mut
fields in structs, and those fields can be mutated. So if I had:
struct Foo {
a: u32,
mut b: u32,
}
and a variable foo: &uniq Foo
, then foo.a += 1
is an error, but foo.b += 1
is ok.
If we had this scheme and we combined it with a notion of multiple kinds of variance, then conceivably we could do:
struct MethodCallContext<'cx, 'qcx> {
typeck_context: &'cx uniq TypeCheckContext<'cx, 'qcx>,
...
}
where TypeCheckContext
would not be invariant with respect to 'cx
, because it too avoids mut
fields.
This is where I start to go off the rails. It seems to me that this notion of &uniq
would be very, very useful – in fact, most of the times we take an &mut
, we probably only need &uniq
. But if you really chase this road, though, I think you arrive at a place where &mut
ought to be basically removed from the language and replaced with &uniq
. Instead of let mut x = 3
you would have some kind of UniqCell
, more like the Ref
type in Ocaml:
struct UniqCell<T> {
pub mut value: T
}
so that I would do let x = UniqCell::new(3);
and then x.value += 1
. Maybe we could do some syntactic sugar.
And here ends my fever dream. I haven’t figured out any path from “here to there” that doesn’t involve massive churn. (Maybe there’s a way to do a transition over various epochs, haven’t thought that hard about it.)