Coming up through the dynamic languages and having my “typed” language be C, I mostly missed out on the whole “generics” thing. Cue the late 2010s and generics are a thing in TypeScript and every so often there’s a push to get generics into PHP. A few years back I figured it was time to learn what was going on with All<Those>
Angle<Brackets>
.
This article is a gentle introduction to generics, and considers what generics syntax might look like in PHP.
Macros/Templates for Classes
Consider a PHP class that collects Animal
objects.
<?php
class Animal {
/* ... */
}
class Collection {
protected $_items=[]; // that's right, an underscore. come at me.
public function add(Animal $animal) {
$this->_items[] = $animal;
}
}
$collection = new Collection;
$animal = new Animal;
$collection->add($animal);
For someone who understands objects and classes this is a straight forward program. We have an add
method, and that add
method has a single parameter, $animal
, which has a type of Animal
. This type tells PHP to only accept Animal
objects into this collection. Try to pass something into add
that’s not an animal, and you’ll get an error.
One trade-off this program makes is our Collection
class can only accept objects of type Animals
. If we try running a program like this
<?php
class Mineral {
/* ... */
}
class Collection {
protected $_items=[]; // that's right, an underscore. come at me.
public function add(Animal $animal) {
$this->_items[] = $animal;
}
}
$collection = new Collection;
$mineral = new Mineral;
$collection->add($mineral);
PHP will hand you back an error something like this
PHP Fatal error: Uncaught TypeError: Argument 1 passed to Collection::add() must be an instance of Animal
If PHP had generics, we wouldn’t have to accept this trade-off. We could use our collection class to create objects that accept anything. If PHP borrowed syntax that’s common in languages with generics, this might look like this
<?php
// as of early 2020, this is not real PHP, but generics exist in
// other langauges like TypeScript, Java, C#, etc. This is
// notional syntax.
class Mineral {
/* ... */
}
class Collection<TypePlaceHolder> {
protected $_items=[]; // that's right, an underscore. come at me.
public function add(TypePlaceHolder $thing) {
$this->_items[] = $thing;
}
}
$animalCollection = new Collection<Animal>;
$animal = new Animal;
$animalCollection->add($animal)
$mineralCollection = new Collection<Mineral>;
$mineral = new Mineral;
$mineralCollection->add($mineral)
$vegetableCollection = new Collection<Mineral>;
$vegetable = new Vegetable;
$vegetableCollection->add($vegetable)
There’s two (or maybe three) new bits of syntax we’ve added here, both use the <>
characters. First, consider this code where we instantiate our object
<?php
// not real php, just notional syntax
$collection = new Collection<Mineral>;
What we’ve done here is added a PHP type/class (Mineral
above) to the end of the class name, and surrounded that type/class name with <>
symbols. When we do this, we’re asking our program to create a Collection
class that accepts Mineral
objects. We could do this with any valid PHP class — Animal
, Mineral
, Vegetable
, etc.
In order to define a class that we can use like this, we need to examine our second bit of new syntax.
<?php
// still not real php but I bet I get email saying this did not
// work but i still love you readers
class Collection<TypePlaceHolder> {
/* ... */
}
After Collection
we’ve added the text TypePlaceHolder
which is, again, surrounded by <>
. This is arbitrary text that we, as the creators of the class, pick. By doing this, we’re telling our program that this class will accept a type during instantiation.
Then (in our third bit of new syntax), we use that place holder text as a type hint.
<?php
// not real PHP, just notional syntax
class Collection<TypePlaceHolder> {
/* ... */
public function add(TypePlaceHolder $thing) {
/* ... */
}
}
When we’re defining our class, this generic type argument becomes a placeholder for whatever we (or other programmers) use when instantiating our class. When someone says
<?php
// not real php, just notional syntax
$collection = new Collection<Animal>
behind the scenes your program will use a class that behaves as though it was written like this
<?php
// not real php, just notional syntax. have you ever really
// just thought about the work notional?
class Collection {
/* ... */
public function add(Animal $thing) {
/* ... */
}
}
The TypePlaceHolder
text is swapped out for an Animal
. Your class can be programmed to work with multiple different object types, but your object still only accepts a single type (Animal
). The advantage is you get to decide when you instantiate your class what that type will be.
Systems that support this technique call it “generic” programming, because end-user-programmers aren’t tied to specific classes. In C++ this concept is called templates, alluding to the way the type argument acts similar to a template placeholder. Apparently Haskall folks call this parametric polymorphism because of course they do.
Conceptually, this is all generics are — placeholder text for types. In practice, generic programming can get gnarly — using generics in return types, accepting multiple types, applying types rules to the generic parameter, etc. Please refer to your programming language’s manual and most popular book for more information.
Generics and Dynamic Languages
It’s worth mentioning that, if you come from a dynamic language, generics can seem like a whole lot of work for nothing. Consider the following class
<?php
// REAL PHP YOU CAN WRITE TODAY
class CollectionAnything {
protected $_items=[]; // that's right, an underscore. come at me.
public function add($thing) {
$this->_items[] = $thing;
}
}
BOOM — no more type hint, a collection of anything! Problem solved!
Programmers coming from typed languages are excited by generics because generics give them a small taste of the freedom that’s available to anyone using a dynamic language without losing their type safety.
Whether that’s the right choice or not is anyone’s guess and ultimately a project-by-project, team-by-team decision.