Recovering Capabilities
A recover
expression let's you "lift" the reference capability of the result. A mutable reference capability (iso
, trn
, or ref
) can become any reference capability, and an immutable reference capability (val
or box
) can become any immutable or opaque reference capability.
Why is this useful?
This most straightforward use of recover
is to get an iso
that you can pass to another actor. But it can be used for many other things as well, such as:
- Creating a cyclic immutable data structure. That is, you can create a complex mutable data structure inside a
recover
expression, "lift" the resultingref
to aval
. - "Borrow" an
iso
as aref
, do a series of complex mutable operations on it, and return it as aniso
again. - "Extract" a mutable field from an
iso
and return it as aniso
.
What does this look like?
recover Array[String] end
That's a simple one: it returns an Array[String] iso
, instead of the usual Array[String] ref
you would get. The reason it is iso
and not any of the other mutable reference capabilities is because there is a default reference capability when you don't specify one. The default for any mutable reference capability is iso
and the default for any immutable reference capability is val
.
Here's a more complicated example from the standard library:
recover
var s = String((prec + 1).max(width.max(31)))
var value = x
try
if value == 0 then
s.push(table(0)?)
else
while value != 0 do
let index = ((value = value / base) - (value * base))
s.push(table(index.usize())?)
end
end
end
_extend_digits(s, prec')
s.append(typestring)
s.append(prestring)
_pad(s, width, align, fill)
s
end
That's from format/_FormatInt
. It creates a String ref
, does a bunch of stuff with it, and finally returns it as a String iso
.
You can also give an explicit reference capability:
let key = recover val line.substring(0, i).>strip() end
That's from net/http/_PayloadBuilder
. We get a substring of line
, which is a String iso^
, then we call strip on it, which returns itself. But since strip is a ref
function, it returns itself as a String ref^
- so we use a recover val
to end up with a String val
.
How does this work?
Inside the recover
expression, your code only has access to sendable values from the enclosing lexical scope. In other words, you can only use iso
, val
and tag
things from outside the recover
expression.
This means that when the recover
expression finishes, any aliases to the result of the expression other than iso
, val
and tag
ones won't exist anymore. That makes it safe to "lift" the reference capability of the result of the expression.
If the recover
expression could access non-sendable values from the enclosing lexical scope, "lifting" the reference capability of the result wouldn't be safe. Some of those values could "leak" into an iso
or val
result, and result in data races.
Automatic receiver recovery
When you have an iso
or trn
receiver, you normally can't call ref
methods on it. That's because the receiver is also an argument to a method, which means both the method body and the caller have access to the receiver at the same time. And that means we have to alias the receiver when we call a method on it. The alias of an iso
is a tag
(which isn't a subtype of ref
) and the alias of a trn
is a box
(also not a subtype of ref
).
But we can get around this! If all the arguments to the method at the call-site are sendable, and the return type of the method is either sendable or isn't used at the call-site, then we can "automatically recover" the receiver. That just means we don't have to alias the receiver - and that means we can call ref
methods on an iso
or trn
, since iso
and trn
are both subtypes of ref
.
Notice that this technique looks mostly at the call-site, rather than at the definition of the method being called. That makes it more flexible. For example, if the method being called wants a ref
argument, and we pass it an iso
argument, that's sendable at the call-site, so we can still do automatic receiver recovery.
This may sound a little complicated, but in practice, it means you can write code that treats an iso
mostly like a ref
, and the compiler will complain when it's wrong. For example:
let s = recover String end
s.append("hi")
Here, we create a String iso
and then append some text to it. The append method takes a ref
receiver and a box
parameter. We can automatically recover the iso
receiver since we pass a val
parameter, so everything is fine.