Skip to content

As Operator

The 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.

as (like 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. F32 and 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 F64 or 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 Critter interface.

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

The 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.