Generics and Reference Capabilities
In the examples presented previously we've explicitly set the reference capability to val
:
class Foo[A: Any val]
If the capability is left out of the type parameter then the generic class or function can accept any reference capability. This would look like:
class Foo[A: Any]
It can be made shorter because Any
is the default constraint, leaving us with:
class Foo[A]
This is what the example shown before looks like but with any reference capability accepted:
// Note - this won't compile
class Foo[A]
var _c: A
new create(c: A) =>
_c = c
fun get(): A => _c
fun ref set(c: A) => _c = c
actor Main
new create(env:Env) =>
let a = Foo[U32](42)
env.out.print(a.get().string())
a.set(21)
env.out.print(a.get().string())
Unfortunately, this doesn't compile. For a generic class to compile it must be compilable for all possible types and reference capabilities that satisfy the constraints in the type parameter. In this case, that's any type with any reference capability. The class works for the specific reference capability of val
as we saw earlier, but how well does it work for ref
? Let's expand it and see:
// Note - this also won't compile
class Foo
var _c: String ref
new create(c: String ref) =>
_c = c
fun get(): String ref => _c
fun ref set(c: String ref) => _c = c
actor Main
new create(env:Env) =>
let a = Foo(recover ref String end)
env.out.print(a.get().string())
a.set(recover ref String end)
env.out.print(a.get().string())
This does not compile. The compiler complains that get()
doesn't actually return a String ref
, but this->String ref
. We obviously need to simply change the type signature to fix this, but what is going on here?
this->String ref
is an arrow type. An arrow type with "this->" states to use the capability of the actual receiver (ref
in our case), not the capability of the method (which defaults to box
here). According to viewpoint adaption this will be ref->ref
which is ref
. Without this arrow type we would only see the field _c
as box
because we are in a box
method.
So let's apply what we just learned:
class Foo
var _c: String ref
new create(c: String ref) =>
_c = c
fun get(): this->String ref => _c
fun ref set(c: String ref) => _c = c
actor Main
new create(env:Env) =>
let a = Foo(recover ref String end)
env.out.print(a.get().string())
a.set(recover ref String end)
env.out.print(a.get().string())
That compiles and runs, so ref
is valid now. The real test though is iso
. Let's convert the class to iso
and walk through what is needed to get it to compile. We'll then revisit our generic class to get it working:
An iso
specific class
// Note - this won't compile
class Foo
var _c: String iso
new create(c: String iso) =>
_c = c
fun get(): this->String iso => _c
fun ref set(c: String iso) => _c = c
actor Main
new create(env:Env) =>
let a = Foo(recover iso String end)
env.out.print(a.get().string())
a.set(recover iso String end)
env.out.print(a.get().string())
This fails to compile. The first error is:
main.pony:5:8: right side must be a subtype of left side
_c = c
^
Info:
main.pony:4:17: String iso! is not a subtype of String iso: iso! is not a subtype of iso
new create(c: String iso) =>
^
The error is telling us that we are aliasing the String iso
- The !
in iso!
means it is an alias of an existing iso
. Looking at the code shows the problem:
new create(c: String iso) =>
_c = c
We have c
as an iso
and are trying to assign it to _c
. This creates two aliases to the same object, something that iso
does not allow. To fix it for the iso
case we have to consume
the parameter. The correct constructor should be:
new create(c: String iso) =>
_c = consume c
A similar issue exists with the set
method. Here we also need to consume the variable c
that is passed in:
fun set(c: String iso) => _c = consume c
Now we have a version of Foo
that is working correctly for iso
. Note how applying the arrow type to the get
method also works for iso
. But here the result is a different one, by applying viewpoint adaptation we get from ref->iso
(with ref
being the capability of the receiver, the Foo
object referenced by a
) to iso
. Through the magic of automatic receiver recovery we can call the string
method on it:
class Foo
var _c: String iso
new create(c: String iso) =>
_c = consume c
fun get(): this->String iso => _c
fun ref set(c: String iso) => _c = consume c
actor Main
new create(env:Env) =>
let a = Foo(recover iso String end)
env.out.print(a.get().string())
a.set(recover iso String end)
env.out.print(a.get().string())
A capability generic class
Now that we have iso
working we know how to write a generic class that works for iso
and it will work for other capabilities too:
class Foo[A]
var _c: A
new create(c: A) =>
_c = consume c
fun get(): this->A => _c
fun ref set(c: A) => _c = consume c
actor Main
new create(env:Env) =>
let a = Foo[String iso]("Hello".clone())
env.out.print(a.get().string())
let b = Foo[String ref](recover ref "World".clone() end)
env.out.print(b.get().string())
let c = Foo[U8](42)
env.out.print(c.get().string())
It's quite a bit of work to get a generic class or method to work across all capability types, in particular for iso
. There are ways of restricting the generic to subsets of capabilities and that's the topic of the next section.