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