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!
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.
Traits: nominal subtyping
Pony has nominal subtyping, 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.
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
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
Baldcategory to be more specific than the
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
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
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.
If you've used Go, you'll recognise that Go interfaces are structural types.
Interfaces: structural subtyping
Pony has structural subtyping too, using interfaces. Interfaces look like traits, but they use the keyword
interface HasName fun name(): String
HasName looks a lot like
Named, except it's an interface instead of a trait. This means both
HasName! The programmers that wrote
Larry don't even have to be aware that
Pony interfaces can have functions with default implementations as well. A type will only pick those up if it explicitly declares that it
is that interface.
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: they stop accidental subtyping.