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 {
owned Money 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 owned Money
. Information about ownership is NOT available at runtime; it is only available during compilation.
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() {
owned Money 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:
owned Money test() {
owned Money 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:
owned Money 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(owned Money >> unowned Money m) { // m is owned initially but must be unowned at the end.
// implementation not shown
};
void testSpend(owned Money >> unowned Money m) {
spend(m);
// m is now of type unowned Money 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(owned Money m)
is equivalent to void test(owned Money >> owned Money m)
. void foo(unowned Money 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:
owned Money m = ...;
owned Money newMoney = m;
// Now m is of type unowned Money, not owned Money.
unowned Money 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(owned Money >> unowned Money m) {
// implementation not shown
};
void print(unowned Money m) {
// implementation not shown
}
void test() {
owned Money m = ...
print(m);
spend(m);
spend(m); // COMPILE ERROR: spend() requires owned input, but m is Unowned here
}
owned Money withdraw() {
// body not shown
}
void test() {
owned Money 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(owned Money >> unowned Money 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(owned Money >> unowned Money this) {
disown this;
}
}
class Wallet {
void throwAwayMoney(owned Money >> unowned Money 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 {
owned Money 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.
owned Money exchange(owned Money >> unowned Money otherMoney) {
owned Money temporaryMoney = money; // OK temporarily...
money = otherMoney; // as long as ownership of money is restored by the end of the method.
return temporaryMoney;
}
}