Our new programming language is object-oriented. As usual, it includes classes, which can have fields. In addition, of the many variables or fields that reference objects, one of them can own the object. For example:
class Money {
}
class Wallet {
Money@Owned m; // @Owned indicates that m owns the object it refers to
}
The compiler tracks ownership of each variable every time the variable is used. This information is part of the type of the variable. For example, the type of m
is Money@Owned
. Information about ownership is NOT available at runtime; it is only available during compilation.
If a reference does not own the object it refers to, then there are two possibilities.
Owned
.Shared
.Unowned
.One benefit of tracking ownership is preventing accidental loss of valuable objects, such as money. To do so, we can declare classes with the keyword asset
. Let's do this for Money
:
asset class Money {
}
Now, if we accidentally lose track of an owning reference to a Money
object (by letting it go out of scope without transferring ownership to somewhere else, such as a field), the compiler will give an error:
void test() {
Money@Owned m = ...; // OK, m is Owned here
// ERROR: cannot drop reference to owned asset m
}
We can fix this by (for example) returning m, assigning it to an owning field, or passing it as an argument to an appropriate method. For example:
Money@Owned test() {
Money@Owned m = ...; // OK, m is Owned here
return m; // gives ownership of m to the caller of test()
}
Note that non-owning references to Money
are not restricted; the compiler gives no errors when they go out of scope.
When methods return objects, the return types can be annotated. For example:
Money@Owned withdraw() {
// body not shown
}
If the return type is unannotated, then @Unowned
is assumed.
When a reference is passed to a method as an argument, the method's declaration specifies initial and final ownership with >>
. For example:
void spend(Money@Owned >> Unowned m) { // m is Owned initially but must be Unowned at the end.
// implementation not shown
};
void testSpend(Money@Owned >> Unowned m) {
spend(m);
// m is now of type Money@Unowned due to specification on spend() declaration.
}
If a method expects an argument that is Unowned
, this means that the method cannot take ownership. As a result, it is safe to pass an Owned
reference as an argument to a method that expects an Unowned
argument. After the method returns, the caller still holds ownership.
If a method parameter lacks >>, then ownership will not change. For example, void test(Money@Owned m)
is equivalent to void test(Money@Owned >> Owned m)
. void foo(Money@Unowned m)
can accept a Money
reference with any ownership and the caller maintains whatever ownership it had initially when it called that method.
If a method parameter lacks an annotation, then @Unowned
is assumed.
Assignment (=
) transfers ownership when the variables on both sides are owning references. That is, the variable on the left becomes the new owner, and the variable on the right loses ownership. For example:
Money@Owned m = ...;
Money@Owned newMoney = m;
// Now m is of type Money@Unowned, not Money@Owned.
Money@Unowned n = newMoney; // Ownership is still with newMoney because n is Unowned
The compiler checks each invocation to make sure it is permitted according to the ownership of the method arguments. For example:
void spend(Money@Owned >> Unowned m) {
// implementation not shown
};
void print(Money@Unowned m) {
// implementation not shown
}
void test() {
Money@Owned m = ...
print(m);
spend(m);
spend(m); // COMPILE ERROR: spend() requires Owned input, but m is Unowned here
}
Money@Owned withdraw() {
// body not shown
}
void test() {
Money@Owned m = withdraw();
spend(m);
}
If ownership is no longer desired, disown
can be used to relinquish ownership. For example:
class Money {
int amount;
void merge(Money@Owned >> Unowned mergeFrom) {
amount += mergeFrom.amount;
disown mergeFrom; // We absorbed the value of mergeFrom, so the owner doesn't own it anymore.
}
}
this
)Sometimes the ownership of this
needs to change in a method. That fact can be specified by adding this
as the first argument in the method declaration. For example:
class Money {
void discard(Money@Owned >> Unowned this) {
disown this;
}
}
class Wallet {
void throwAwayMoney(Money @ Owned >> Unowned money) {
money.discard(); // 'this' argument is implicit; do not include it in method call.
}
}
Field declarations MUST include ownership specifications. These mean that at the END of each method, the state of the object the field references MUST match the annotation on the field. For example:
class Wallet {
Money@Owned money; // Note that this annotation is on a field declaration!
// exchange removes (and returns) the current contents of the Wallet, replacing it with the input Money.
Money@Owned exchange(Money@Owned >> Unowned otherMoney) {
Money@Owned temporaryMoney = money; // OK temporarily...
money = otherMoney; // as long as money@Owned is restored by the end of the method.
return temporaryMoney;
}
}