Capability Subtyping
Subtyping is about substitutability. That is, if we need to supply a certain type, what other types can we substitute instead? Reference capabilities factor into this.
Simple substitution
First, let's cover substitution without worrying about ephemeral types (^
) or alias types (!
). The <:
symbol means "is a subtype of" or alternatively "can be substituted for".
iso <: trn
. Aniso
is read and write unique, and atrn
is just write unique, so it's safe to substitute aniso
for atrn
.trn <: ref
. Atrn
is mutable and also write unique. Aref
is mutable but makes no uniqueness guarantees. It's safe to substitute atrn
for aref
.trn <: val
. This one is interesting. Atrn
is write unique and aval
is globally immutable, so why is it safe to substitute atrn
for aval
? The key is that, in order to do so, you have to give up thetrn
you have. If you give up the only variable that can write to an object, you know that no variable can write to it. That means it's safe to consider it globally immutable.ref <: box
. Aref
guarantees no other actor can read from or write to the object. Abox
just guarantees no other actor can write to the object, so it's safe to substitute aref
for abox
.val <: box
. Aval
guarantees no actor, not even this one, can write to the object. Abox
just guarantees no other actor can write to the object, so it's safe to substitute aval
for abox
.box <: tag
. Abox
guarantees no other actor can write to the object, and atag
makes no guarantees at all, so it's safe to substitute abox
for atag
.
Subtyping is transitive. That means that since iso <: trn
and trn <: ref
and ref <: box
, we also get iso <: box
.
Aliased substitution
Now let's consider what happens when we have an alias of a reference capability. For example, if we have some iso
and we alias it (without doing a consume
or a destructive read), the type we get is iso!
, not iso
.
iso! <: tag
. This is a pretty big change. Instead of being a subtype of everything likeiso
, the only thing aniso!
is a subtype of istag
. This is because theiso
still exists, and is still read and write unique. Any alias can neither read from nor write to the object. That means aniso!
can only be a subtype oftag
.trn! <: box
. This is a change too, but not as big a change. Sincetrn
is only write unique, it's ok for aliases to read from the object, but it's not ok for aliases to write to the object. That means we could havebox
orval
aliases - exceptval
guarantees that no alias can write to the object! Since ourtrn
still exists and can write to the object, aval
alias would break the guarantees thatval
makes. So atrn!
can only be a subtype ofbox
(and, transitively,tag
as well).ref! <: ref
. Since aref
only guarantees that other actors can neither read from nor write to the object, it's ok to make moreref
aliases within the same actor.val! <: val
. Since aval
only guarantees that no actor can write to the object, its ok to make moreval
aliases since they can't write to the object either.box! <: box
. Abox
only guarantees that other actors can't write to the object. Bothval
andref
make that guarantee too, so why canbox
only alias asbox
? It's because we can't make more guarantees when we alias something. That meansbox
can only alias asbox
.tag! <: tag
. Atag
doesn't make any guarantees at all. Just like with abox
, we can't make more guarantees when we make a new alias, so atag
can only alias as atag
.
Ephemeral substitution
The last case to consider is when we have an ephemeral reference capability. For example, if we have some iso
and we consume
it or do a destructive read, the type we get is iso^
, not iso
.
iso^ <: iso
. This is pretty simple. When we give aniso^
a name, by assigning it to something or passing it as an argument to a method, it loses the^
and becomes a plain oldiso
. We know we gave up our previousiso
, so it's safe to have a new one.trn^ <: trn
. This works exactly likeiso^
. The guarantee is weaker (write uniqueness instead of read and write uniqueness), but it works the same way.ref^ <: ref^
andref^ <: ref
andref <: ref^
. Here, we have another case. Not only is aref^
a subtype of aref
, it's also a subtype of aref^
. What's going on here? The reason is that an ephemeral reference capability is a way of saying "a reference capability that, when aliased, results in the base reference capability". Since aref
can be aliased as aref
, that meansref
andref^
are completely interchangeable.val^
,box^
,tag^
. These all work the same way asref
, that is, they are interchangeable with the base reference capability. It's for the same reason: all of these reference capabilities can be aliased as themselves.
Why do ref^
, val^
, box^
, and tag^
exist if they are interchangeable with their base reference capabilities? It's for two reasons: reference capability recovery and generics. We'll cover both of those later.