2016-05-04
Bang, Hat and Arrow in Pony
If you've looked at Pony programming language code you've probably seen use of punctuation symbols in places and wondered what they meant. This post is an attempt to explain three of those - the bang, hat and arrow (!
, ^
and ->
respectively). Note that this is my understanding based on usage, reading the tutorials and watching videos so there may be errors. I welcome corrections!
Bang
The bang symbol (otherwise known as an exclamation mark) combined with a type name can be thought of as the type of an alias of the given type. Having an alias of an object means having another reference to that object. So an alias to a String iso
is of type String iso!
. This matters mostly in generic code which will be explained later but it does come up in error messages.
If you see !
in an error message like "iso! is not a subtype of iso" this means you are probably trying to assign an object that cannot be aliased without first consuming it.
If you see !
in a type declaration in code like "let foo: A!" then you can read this as "replace A! with a type that can safely hold an alias to A". If A
is a String iso
then A!
would be a String tag
for example (following the rules for aliased substitution.
Bang in errors
The following code demonstrates something that is often encountered by first time Pony users:
class Something
actor Main
new create(env: Env) =>
let a = recover iso Something end
Foo(a)
actor Foo
new create(s: Something iso) =>
None
Here we have a class called Something
. A new instance of it is created in the Main actor with reference capability iso
. A new Foo
actor is created passing this instance to it. This will fail to compile as we are aliasing the Something
object held in a
. a
holds a reference to it and the variable s
holding the argument to the Foo
constructor is holding a reference to it at the same time. Objects with a reference capability of iso
cannot have more than one reference to it. The error from the compiler will look like:
Error:
e1/main.pony:6:9: argument not a subtype of parameter
Foo(a)
^
Info:
e1/main.pony:9:14: parameter type: Something iso
new create(s: Something iso) =>
^
e1/main.pony:6:9: argument type: Something iso!
Foo(a)
^
e1/main.pony:1:1: Something iso! is not a subtype of Something iso: iso! is not a subtype of iso
This error states that the expected type of the parameter for the Foo
constructor is of type Something iso
but the type that we passed is a Something iso!
. It further explains things by noting that Something iso!
is not a subtype of Something iso
because iso!
is not a subtype of iso
.
Armed with the knowledge that the bang symbol means the type for an alias this can be read as the argument passed was an alias to a Something iso
. This is an error as iso
cannot be aliased - this is what iso! is not a subtype of iso
means. The subtyping relationship for aliases is outlined in the Capability Subtyping section of the tutorial.
The code can be fixed by consuming the a
so it is no longer aliased:
let a = recover iso Something end
Foo(consume a)
Bang in generics
The other place where you'll see the alias type is in generic code. The following non-generic code compiles fine:
class Something
let a: U8
new create(x: U8) =>
a = x
actor Main
new create(env: Env) =>
let aint = Something(42)
U8
defaults to val
reference capability which can be aliased. This allows the assignment to the field a
in Something
which is aliasing the x
object. If we make this a generic so that any type can be used then it fails to compile:
class Something[A]
let a: A
new create(x: A) =>
a = x
actor Main
new create(env: Env) =>
let aint = Something[U8](42)
The error is:
Error:
e3/main.pony:5:7: right side must be a subtype of left side
a = x
^
Info:
e3/main.pony:4:17: right side type: A #any !
new create(x: A) =>
^
e3/main.pony:5:5: left side type: A #any
a = x
^
e3/main.pony:4:17: A #any ! is not a subtype of A #any: the subtype has no constraint
new create(x: A) =>
^
For now, ignore the #any
in the error message. I'll expand on this later but it's informing us that the type A
is unconstrained and can have any reference capability.
The error states that x
is an A!
but a
is an A
and A!
is not a subtype of A
so the assignment cannot happen.
This occurs Because A
is unconstrained. It can be any reference capability. Therefore the code must be able to be compiled under the assumption that the most restrictive reference capability can be used. It works fine with val
, which can be aliased, but not with iso
which cannot. Therefore the generic code cannot be compiled. You can see how iso
would fail by expanding a version using String iso
:
class Something
let a: String iso
new create(x: String iso) =>
a = x
actor Main
new create(env: Env) =>
let aint = Something(recover iso String end)
The error is:
Error:
e5/main.pony:5:7: right side must be a subtype of left side
a = x
^
Info:
e5/main.pony:4:17: right side type: String iso!
new create(x: String iso) =>
^
e5/main.pony:2:10: left side type: String iso
let a: String iso
^
e5/main.pony:4:17: String iso! is not a subtype of String iso: iso! is not a subtype of iso
new create(x: String iso) =>
^
This is the same error that the generic code is giving us. The generic code can be fixed in a few ways. The first is to constrain the type so that it is a specific reference capability that works. Here it is changed to val
:
class Something[A: Any val]
let a: A
new create(x: A) =>
a = x
actor Main
new create(env: Env) =>
let aint = Something[U8](42)
The A: Any val
syntax constrains the type parameter to be a subtype of the type after the :
. In this case, any type with a reference capability of val. This won't work if you want to be able to use any aliasable type (eg ref
as well as val
):
class Something[A: Any val]
let a: A
new create(x: A) =>
a = x
actor Main
new create(env: Env) =>
let aint = Something[U8](42)
let bint = Something[String ref](recover ref String end)
The error here is obvious in that we are trying to pass a ref
parameter to a function expecting a val
. Pony generics solves this by allowing code to be polymorphic over the reference capability. There are specific annotations for classes of reference capabilities. They are:
#read = { ref, val, box } = Anything you can read from
#send = { iso, val, tag } = Anything you can send to an actor
#share = { val, tag } = Anything you can send to more than one actor
#any = { iso, trn, ref, val, box, tag } = Default of a constraint
#alias = {ref,val, box, tag} = Set of capabilities that alias as themselves (used by compiler)
A version that will work for ref
, val
and box
becomes:
class Something[A: Any #read]
let a: A
new create(x: A) =>
a = x
actor Main
new create(env: Env) =>
let aint = Something[U8](42)
let bint = Something[String ref](recover ref String end)
But what if you want it to work with non-aliasable types like iso
? A solution is to consume
the parameter:
class Something[A]
let a: A
new create(x: A) =>
a = consume x
actor Main
new create(env: Env) =>
let aint = Something[U8](42)
let bint = Something[String ref](recover ref String end)
let cint = Something[String iso](recover iso String end)
Another solution is to declare the field type to be A!
instead of A
. In the String iso
case using A
means String iso
which cannot hold an alias. Using A!
means String iso!
which should be read as "a type that can safely alias a String iso
". Looking at the Aliased substitution table this is a tag
:
class Something[A]
let a: A!
new create(x: A) =>
a = x
actor Main
new create(env: Env) =>
let aint = Something[U8](42)
let bint = Something[String ref](recover ref String end)
let cint = Something[String iso](recover iso String end)
In this case we are using !
to tell the compiler to use a reference capability that works for whatever the type of A
is. An iso
becomes a tag
, a trn
becomes a box
, a ref
stays a ref
, etc.
Hat
The hat symbol (or ^
) is an ephemeral type. It's the type of an object that is not assigned to a variable. consume x
is used to prevent aliasing of x
but at the point of being consumed and before it is assigned to anything else, what type is it? If x
is type A
then the type of consume x
is A^
. Constructors always return an ephemeral type as they create objects and return them but they aren't yet assigned to anything.
The following example creates a Box
type that acts like single instance array of String iso
objects. A value can be stored and updated. A utility function Foo.doit
takes a String iso
as an argument. It's stubbed out since it doesn't need to do anything for the example. The main code creates a Box
, updates it, and calls the utility function on it.
class Box
var a: String iso
new create(x: String iso) =>
a = consume x
fun ref update(x: String iso): String iso =>
let b = a = consume x
consume b
primitive Foo
fun doit(s: String iso) =>
None
actor Main
new create(env: Env) =>
let a = Box(recover iso String end)
let b = a.update(recover iso String end)
Foo.doit(consume b)
Some things to note based on prior discussion. The create
method consumes the argument to prevent aliasing. The update
function also consumes the argument to prevent aliasing. It uses the destructive read syntax to assign the argument x
to the field a
and assign the old value of a
to b
to avoid aliasing. Unfortunately this example fails to compile:
Error:
f3/main.pony:19:14: argument not a subtype of parameter
Foo.doit(consume b)
^
Info:
f3/main.pony:12:12: parameter type: String iso
fun doit(s: String iso) =>
^
f3/main.pony:19:14: argument type: String iso!
Foo.doit(consume b)
^
f3/main.pony:7:34: String iso! is not a subtype of String iso: iso! is not a subtype of iso
fun ref update(x: String iso): String iso =>
^
From the discussion previously on !
this error tells us that we are aliasing b
. We can narrow it down by explicitly declaring the type of b
:
let b: String iso = a.update(recover iso String end)
The error is due to update
returning a String iso
. We are consuming b
and returning it as a String iso
which then gets aliased when assigned to b
in the main routine. Changing the return type to use hat resolves the issue. consume b
returns the ephmeral type which is an object with no variable referencing it. It is safe to assign to a String iso
so the change compiles:
class Box
var a: String iso
new create(x: String iso) =>
a = consume x
fun ref update(x: String iso): String iso^ =>
let b = a = consume x
consume b
primitive Foo
fun doit(s: String iso) =>
None
actor Main
new create(env: Env) =>
let a = Box(recover iso String end)
let b = a.update(recover iso String end)
Foo.doit(consume b)
Another approach would be to return a String iso
but change doit
to be a String tag
(the type that can alias a String iso
). This compiles but because doit
now takes String tag
it is limited in what it can do with the string. The approach of using an ephemeral type allows obtaining the mutable object from the Box
.
Hat in parameters
Sometimes you'll see hat in parameter lists. The Array
builtin has an init
constructor that looks like:
new init(from: A^, len: USize)
This initializes an array so that all elements are the from
value. To explore how this works, here's a smaller example that does something similar:
class Box[A]
var a: A
var b: A
new create(x: A^) =>
a = x
b = x
Without the hat in A^
there is an error due to aliasing. We can't assign x
to both a
and b
in case A
is an iso
. With the hat it compiles. The is because an epehemeral reference capability is a way of saying "a reference capability that, when aliased, results in the base reference capability". So a String iso^
can be assigned to a String iso
, a String ref^
can be assigned to a String ref
, etc. This means the generic class itself compiles but using it for a String iso
will fail due to aliasing but it can be used for other reference capability types. Compare this to a plain x: A
where the generic class itself won't compile since a String iso
can't be assigned to another String iso
due to aliasing.
Code demonstrating this is:
actor Main
new create(env: Env) =>
let a = Box[String iso](recover iso String end)
let c = Box[String ref](recover ref String end)
let e = Box[String val](recover val String end)
Arrow
The arrow syntax (or ->
) is known as viewpoint adapter types and is related to viewpoint adaption.
Arrow in error messages
Viewpoint adaption defines what the reference capability of a field looks like to some caller based on the reference capability of the object the field is being read from. This is important to maintain the reference capability guarantees. A val
object should not be able to access an iso
field as iso
or it breaks the constraints of val
- it should be immutable but obtaining it as iso
allows mutation of the field. There is a table in viewpoint adaption that shows what the mapping is.
An example of an error that can occur by ignoring viewpoint adaption is in the following code:
class Something
var a: String iso
new create() =>
a = recover iso String end
fun doit(s: String) =>
a.append(s)
actor Main
new create(env: Env) =>
let a = Something
a.doit("hello")
The error here is calling append
on a the a
field in the doit
method. By default methods have a receiver reference capability of box
. Anything that happens inside the method cannot affect the state of the object. This is why you see methods that modify object fields start with fun ref
- it's to change the receiver reference capability to something mutable. Even though the field a
is iso
and therefore mutable because we are inside a box
method it appears as a non-mutable reference capability. The viewpoint adaption table shows that a box
origin with an iso
field gives a tag
type. So a
looks like a String tag
within the method. The compiler gives:
Error:
v/main.pony:8:13: receiver type is not a subtype of target type
a.append(s)
^
Info:
v/main.pony:8:5: receiver type: this->String iso!
a.append(s)
^
ny/ponyc/packages/builtin/string.pony:622:3: target type: String ref
fun ref append(seq: ReadSeq[U8], offset: USize = 0, len: USize = -1)
^
v/main.pony:2:10: String tag is not a subtype of String ref: tag is not a subtype of ref
var a: String iso
^
The 'receiver type' of this->String iso!
is an example of arrow usage. It's saying that an object of type String iso!
(an alias to a String iso
) as seen by an origin of this
. The reference capability of this
in a method is that of the receiver reference capability on the function - in this case box
. So this->String iso!
is String tag
. That's why the last error description line refers to String tag
.
The solution here is to change the reference capability for the method to something that allows mutation:
fun ref doit(s: String) =>
Arrow in type declarations
When writing generic code it's sometimes required to be explicit in what viewpoint adaption to use for generic types. Returning to the Box
example used previously we'll make it generic and make it usable for any reference capability:
class Box[A]
var a: A
new create(x: A) =>
a = consume x
fun apply(): this->A! =>
a
fun ref update(x: A): A^ =>
let b = a = consume x
consume b
fun clone(): Box[this->A!] =>
Box[this->A!](a)
Notice the use of this->A!
in the return type of apply
. We want to return what is held in the Box
. If it is a Box[String val] val
then we can return a String val
since it is immutable and the box is immutable. If it is a Box[String ref] val
we still want to return a String val
, not a String ref
. The latter would allow modifying an immutable box. If it's a Box[String ref] ref
then it's safe to return a String ref
. This is what the arrow type handles for us. The this
refers to the reference capability of the object. The A!
refers to the field type - note that it is being aliased here so we want a type that can hold an alias to an A
. The viewpoint adaption gives the resulting reference capability of the type.
Looking up the table of viewpoint adaption gives:
Box[String val] val => val->val => val => String val
Box[String ref] val => val->ref => val => String val
Box[String ref] ref => ref->ref => ref => String ref
Box[String iso] ref => ref->iso => iso => String iso
Box[String ref] iso => iso->ref => tag => String tag
That last one is interesting in that the Box[String ref] iso
says that only one reference of the Box
can exist. If we allow a String ref
to be obtained from it then it breaks this condition since both the original reference to the box can modify the string and so can the returned reference. This is why the viewpoint adaption gives a String tag
. A tag
only allows identity operations so it's safe to have this type of alias of an iso
.
Note that the table above gives the mapping for this->A
. Because it's a this->A!
it has to be a type that can hold an alias to the type of the table. So we have another mapping:
String val! => String val
String ref! => String ref
String iso! => String tag
In this way a Box[String iso] ref
will give out a String tag
- the only safe way of aliasing the original string in the box.
The other use of an arrow type in this example is in the clone
function. This must do a shallow copy of the object. It returns a new Box
holding a reference to the same value. Because we need to alias the value the same constraints as described for the apply
method exist. We want to return a Box[this->A!]
to ensure the value object for that box instance is a safe alias to the original. For a Box[String iso] ref
this returns a Box[String tag]
for example.
The following code can be used with the Box
class above to test it:
primitive Foo
fun doit(s: String tag) =>
None
actor Main
new create(env: Env) =>
let a = Box[String iso](recover iso String end)
let b = a.clone()
Foo.doit(b())
let c = Box[String ref](recover ref String end)
let d = c.clone()
Foo.doit(d())
let e = Box[String val](recover val String end)
let f = e.clone()
Foo.doit(f())
A->B arrows
Arrow types don't need to always use this
on the receiver side. They can use an explicit reference capability like box->A
or they can use another parameterized type. Examples of this are in some of the library code:
class ArrayValues[A, B: Array[A] #read] is Iterator[B->A]
An ArrayValues
is returned by the values
method on Array
. It's an iterator over the objects in the array. The B->A
syntax means that the type of the generic argument to Iterator
is of type "A
as seen by B
" using viewpoint adaption. It's not an iterator over A
, it's an iterator over "A as seen by B". This allows iteration over arrays whether they are val
or ref
and produces a compatible type for the Iterator
that works with both.
Conclusion
Most of the functionality described here is some of the more esotoric Pony functionality. It is mainly hit when using generics. The best current reference for generics is a video by Sylvan Clebsch for the virtual Pony users group - Writing Generic Code.
A good way to learn is to try some of the examples in this post and play around with them. Try aliasing, using different types, different reference capabilities and see what happens. The Pony library code, Array.pony for example, is a useful reference.