Skip to content

Traits and Interfaces

Like other object-oriented languages, Pony has subtyping. That is, some types serve as categories that other types can be members of.

There are two kinds of subtyping in programming languages: nominal and structural. They’re subtly different, and most programming languages only have one or the other. Pony has both!

Nominal subtyping

This kind of subtyping is called nominal because it is all about names.

If you’ve done object-oriented programming before, you may have seen a lot of discussion about single inheritance, multiple inheritance, mixins, traits, and similar concepts. These are all examples of nominal subtyping.

The core idea is that you have a type that declares it has a relationship to some category type. In Java, for example, a class (a concrete type) can implement an interface (a category type). In Java, this means the class is now in the category that the interface represents. The compiler will check that the class actually provides everything it needs to.

In Pony, nominal subtyping is done via the keyword is. is declares at the point of declaration that an object has a relationship to a category type. For example, to use nominal subtyping to declare that the class Name provides Stringable, you’d do:

class Name is Stringable

Structural subtyping

There’s another kind of subtyping, where the name doesn’t matter. It’s called structural subtyping, which means that it’s all about how a type is built, and nothing to do with names.

A concrete type is a member of a structural category if it happens to have all the needed elements, no matter what it happens to be called.

Structural typing is very similar to duck typing from dynamic programming languages, except that type compatibility is determined at compile time rather than at run time. If you’ve used Go, you’ll recognise that Go interfaces are structural types.

You do not declare structural relationships ahead of time, instead it is done by checking if a given concrete type can fulfill the required interface. For example, in the code below, we have the interface Stringable from the standard library. Anything can be used as a Stringable so long as it provides the method fun string(): String iso^. In our example below, ExecveError provides the Stringable interface and can be used anywhere a Stringable is needed. Because Stringable is a structural type, ExecveError doesn’t have to declare a relationship to Stringable, it simply has that relationship because it has “the same shape”.

interface box Stringable
  """
  Things that can be turned into a String.
  """
  fun string(): String iso^
    """
    Generate a string representation of this object.
    """

primitive ExecveError
  fun string(): String iso^ => "ExecveError".clone()

Nominal and structural subtyping in Pony

When discussing subtyping in Pony, it is common to say that trait is nominal subtyping and interface is structural subtyping, however, that isn’t really true.

Both trait and interface can establish a relationship via nominal subtyping. Only interface can be used for structural subtyping.

Nominal subtyping in Pony

The primary means of doing nominal subtyping in Pony is using traits. A trait looks a bit like a class, but it uses the keyword trait and it can’t have any fields.

trait Named
  fun name(): String => "Bob"

class Bob is Named

Here, we have a trait Named that has a single function name that returns a String. It also provides a default implementation of name that returns the string literal “Bob”.

We also have a class Bob that says it is Named. This means Bob is in the Named category. In Pony, we say Bob provides Named, or sometimes simply Bob is Named.

Since Bob doesn’t have its own name function, it uses the one from the trait. If the trait’s function didn’t have a default implementation, the compiler would complain that Bob had no implementation of name.

trait Named
  fun name(): String => "Bob"

trait Bald
  fun hair(): Bool => false

class Bob is (Named & Bald)

It is possible for a class to have relationships with multiple categories. In the above example, the class Bob provides both Named and Bald.

trait Named
  fun name(): String => "Bob"

trait Bald is Named
  fun hair(): Bool => false

class Bob is Bald

It is also possible to combine categories together. In the example above, all Bald classes are automatically Named. Consequently, the Bob class has access to both hair() and name() default implementation of their respective trait. One can think of the Bald category to be more specific than the Named one.

class Larry
  fun name(): String => "Larry"

Here, we have a class Larry that has a name function with the same signature. But Larry does not provide Named!

Wait, why not? Because Larry doesn’t say it is Named. Remember, traits are nominal: a type that wants to provide a trait has to explicitly declare that it does. And Larry doesn’t.

You can also do nominal subtyping using the keyword interface. Interfaces in Pony are primarily used for structural subtyping. Like traits, interfaces can also have default method implementations, but in order to use default method implementations, an interface must be used in a nominal fashion. For example:

interface HasName
  fun name(): String => "Bob"

class Bob is HasName

class Larry
  fun name(): String => "Larry"

Both Bob and Larry are in the category HasName. Bob because it has declared that it is a HasName and Larry because it is structurally a HasName.

Structural subtyping in Pony

Pony has structural subtyping using interfaces. Interfaces look like traits, but they use the keyword interface.

interface HasName
  fun name(): String

Here, HasName looks a lot like Named, except it’s an interface instead of a trait. This means both Bob and Larry provide HasName! The programmers that wrote Bob and Larry don’t even have to be aware that HasName exists.

Differences between traits and interfaces

It is common for new Pony users to ask, Should I use traits or interfaces in my own code? Both! Interfaces are more flexible, so if you’re not sure what you want, use an interface. But traits are a powerful tool as well.

Private methods

A key difference between traits and interfaces is that interfaces can’t have private methods. So, if you need private methods, you’ll need to use a trait and have users opt in via nominal typing. Interfaces can’t have private methods because otherwise, users could use them to break encapsulation and access private methods on concrete types from other packages. For example:

actor Main
  new create(env: Env) =>
    let x: String ref = "sailor".string()
    let y: Foo = x
    y._set(0, 'f')
    env.out.print("Hello, " + x)

interface Foo
  fun ref _set(i: USize, value: U8): U8

In the code above, the interface Foo allows access to the private _set method of String and allows for changing sailor to failor or it would anyway, if interfaces were allowed to have private methods.

Open world enumerations

Traits allow you to create “open world enumerations” that others can participate in. For example:

trait Color

primitive Red is Color
primitive Blue is Color

Here we are using a trait to provide a category of things, Color, that any other types can opt into by declaring itself to be a Color. This creates an “open world” of enumerations that you can’t do using the more traditional Pony approach using type unions.

primitive Red
primitive Blue

type Color is (Red | Blue)

In our trait based example, we can add new colors at any time. With the type union based approach, we can only add them by modifying definition of Color in the package that provides it.

Interfaces can’t be used for open world enumerations. If we defined Color as an interface:

interface Color

Then literally everything in Pony would be a Color because everything matches the Color interface. You can however, do something similar using “marker methods” with an interface:

interface Color
  fun is_color(): None

primitive Red
  fun is_color(): None => None

Here we are using structural typing to create a collection of things that are in the category Color by providing a method that “marks” being a member of the category: is_color.

Open world typing

We’ve covered a couple ways that traits can be better than interfaces, let’s close with the reason for why we say, unless you have a reason to, you should use interface instead of trait. Interfaces are incredibly flexible. Because traits only provide nominal typing, a concrete type can only be in a category if it was declared as such by the programmer who wrote the concrete type. Interfaces allow you to create your own categorizations on the fly, as you need them, to group existing concrete types together however you need to.

Here’s a contrived example:

interface Compactable
  fun ref compact()
  fun size(): USize

class Compactor
  """
  Compacts data structures when their size crosses a threshold
  """
  let _threshold: USize

  new create(threshold: USize) =>
    _threshold = threshold

  fun ref try_compacting(thing: Compactable) =>
    if thing.size() > _threshold then
      thing.compact()
    end

The flexibility of interface has allowed us to define a type Compactable that we can use to allow our Compactor to accept a variety of data types including Array, Map, and String from the standard library.