as operator in Pony has two related uses. First, it provides a safe way to increase the specificity of an object’s type. Second, it gives the programmer a way to specify the type of the items in an array literal.
Expressing a different type of an object¶
In Pony, each object is an instance of a single concrete type, which is the most specific type for that object. But the object can also be held as part of a “wider” abstract type such as an interface, trait, or type union which the concrete type is said to be a subtype of.
match) allows a program to check the runtime type of an abstract-typed value to see whether or not the object matches a given type which is more specific. If it doesn’t match the more specific type, then a runtime
error is raised. For example:
class Cat fun pet() => ... type Animal is (Cat | Fish | Snake) fun pet(animal: Animal) => try // raises error if not a Cat let cat: Cat = animal as Cat cat.pet() end
In the above example, within
pet our current view of
animal is via the type union
Animal. To treat
animal as a cat, we need to do a runtime check that the concrete type of the object instance is
Cat. If it is, then we can pet it. This example is a little contrived, but hopefully elucidates how
as can be used to take a type that is a union and get a specific concrete type from it.
Note that the type requested as the
as argument must exist as a type of the object instance, unlike C casting where one type can be forced to become another type. Coercion from one concrete type to another is not possible using
as, so one can not do
let value:F64 = F32(1.0) as F64.
F64 are both concrete types and each object can only have a single concrete type. Many concrete types do provide methods that allow you to convert them to another concrete type, for example,
F32(1.0).f64() to convert an
F32 to an
F32(1.0).string() to convert to a string.
In addition to using
as with a union of disjoint types, we can also express an intersected type of the object, meaning the object has a type that the alias we have for the object is not directly related to the type we want to express. For example:
trait Alive trait Well class Person is (Alive & Well) class LifeSigns fun is_all_good(alive: Alive)? => // if the instance 'alive' is also of type 'Well' (such as a Person instance). raises error if not possible let well: Well = alive as Well
as can also be used to get a more specific type of an object from an alias to it that is an interface or a trait. Let’s say, for example, that you have a library for doing things with furry, rodent-like creatures. It provides a
Critter interface which programmers can then use to create specific types of critters.
interface Critter fun wash(): String
The programmer uses this library to create a
Wombat and a
Capybara class. But the
Capybara class provides a new method,
swim(), that is not part of the
Critter class. The programmer wants to store all of the critters in an array, in order to carry out actions on groups of critters. Now assume that when capybaras finish washing they want to go for a swim. The programmer can accomplish that by using
as to attempt to use each
Critter object in the
Array[Critter] as a
Capybara. If this fails because the
Critter is not a
Capybara, then an error is raised; the program can swallow this error and go on to the next item.
interface Critter fun wash(): String class Wombat is Critter fun wash(): String => "I'm a clean wombat!" class Capybara is Critter fun wash(): String => "I feel squeaky clean!" fun swim(): String => "I'm swimming like a fish!" actor Main new create(env: Env) => let critters = Array[Critter].>push(Wombat).>push(Capybara) for critter in critters.values() do env.out.print(critter.wash()) try env.out.print((critter as Capybara).swim()) end end
You can do the same with interfaces as well. In the example below, we have an Array of
Any which is an interface where we want to try wash any entries that conform to the
actor Main new create(env: Env) => let anys = Array[Any ref].>push(Wombat).>push(Capybara) for any in anys.values() do try env.out.print((any as Critter).wash()) end end
Note, All the
as examples above could be written using a
match statement where a failure to match results in
error. For example, our last example written to use
match would be:
actor Main new create(env: Env) => let anys = Array[Any ref].>push(Wombat).>push(Capybara) for any in anys.values() do try match any | let critter: Critter => env.out.print(critter.wash()) else error end end end
Thinking of the
as keyword as “an attempt to match that will error if not matched” is a good mental model to have. If you don’t care about handling the “not matched” case that causes an error when using
as, you can rewrite an
as to use match without an error like:
actor Main new create(env: Env) => let anys = Array[Any ref].>push(Wombat).>push(Capybara) for any in anys.values() do match any | let critter: Critter => env.out.print(critter.wash()) end end
You can learn more about matching on type in the captures section of the match documentation.
Specify the type of items in an array literal¶
as operator can also be used to tell the compiler what type to use for the items in an array literal. In many cases, the compiler can infer the type, but sometimes it is ambiguous.
For example, in the case of the following program, the method
foo can take either an
Array[U32] ref or an
Array[U64] ref as an argument. If a literal array is passed as an argument to the method and no type is specified then the compiler cannot deduce the correct one because there are two equally valid ones.
actor Main fun foo(xs: (Array[U32] ref | Array[U64] ref)): Bool => // do something boring here true new create(env: Env) => foo([as U32: 1; 2; 3]) // the compiler would complain about this: // foo([1; 2; 3])
The requested type must be a valid type for the items in the array. Since these types are checked at compile time they are guaranteed to work, so there is no need for the programmer to handle an error condition.