Generics in PXP

28 Jul 2023

An overview of generic programming in PXP and the syntax that can be used.

One of the main features that I want to bring to the table with PXP is support for generics syntax. This is something that has been discussed heavily in the PHP community and one of the features that a lot of people are still looking for.

It's probably best to explain what generics are first, since some people have never been exposed to them. I'll give you my explanation, not the explanation from Wikipedia.

Regular functions and methods can define a set of parameters. These parameters can have a type associated with them, but the type can only be defined one. Generic programming allows you to create "type parameters" and change the types associated with a function or method based on how you are invoking the function or method.

The "Hello, world" of generic programming is the identity() function. This is a function that accepts a single parameter and just returns it.

function identity($a) {
    return $a;
}

This function is technically type-safe at runtime since the value we're passing through is always going to be the value returned, but the function doesn't declare that.

In regular PHP code, without any help from static analysers, we can only say that $a is mixed and that identity() returns mixed. To make it statically type-safe, we would need to create multiple versions of the function with the appropriate types, or bring in a static analysis tool such as PHPStan and add DocBlock tags.

/**
 * @template T
 *
 * @param  T  $a
 * @return T
 */
function identity(mixed $a): $mixed {
    return $a;
}

Static analysers can use those DocBlock tags as the "true" source of type information for the parameter and return type and correctly deduce calls to that function during analysis.

PXP plans to introduce a dedicated syntax for generics. The syntax chosen is inspired by an old draft RFC to PHP.

To make the identity() function generic, we can add a type parameter T to the function and then reference that type parameter for the parameter and return type.

function identity<T>(T $a): T {
    return $a;
}

Type parameters for function are declared inside of <>, directly after the function name. The same syntax would apply for methods on classes, interfaces, traits and enums.

When PXP transpiles this code, it would strip the generics from the function completely and produce a function that looks like the very first version we wrote. No type hints, just a plain PHP function.

Classes and other class-like structures can also define type parameters. The "Hello, world" of generic programming in OOP languages is the Box class. This is just a class that wraps a single value and has a method for retrieving the underlying value.

class Box
{
    protected $value;
	
    public function __construct($value)
    {
        $this->value = $value;
    }
	
    public function get()
    {
        return $this->value;
    }
}

To help static analysers understand this generic code, we could add DocBlocks to the class, properties and methods.

/**
 * @template T
 */
class Box
{
    /** @var T */
    protected $value;
	
    /**
     * @param T $value
     */
    public function __construct($value)
    {
        $this->value = $value;
    }
	
    /**
     * @return T
     */
    public function get()
    {
        return $this->value;
    }
}

Translating this into PXP's generic syntax would produce something like the following.

class Box<T>
{
    protected T $value;
	
    public function __construct(T $value)
    {
        $this->value = $value;
    }
	
    public function get(): T
    {
        return $this->value;
    }
}

Shorter, more readable and a more familiar generic syntax.

Type parameters follow the name of the structure inside of <>, the same as functions and methods. The type parameters can then be used as property types, parameter types and return types.

Type parameters can also be constrained. Let's introduce a Boxable interface that can be implemented by a class and then used as a marker to constrain which values Box accepts.

interface Boxable
{
    // ...
}

class MyValue implements Boxable
{
    // ...
}

Using a new is keyword, we can limit the T type parameter to a specific type string.

class Box<T is Boxable>
{
    // ...
}

This now limits the accepted values to just objects that implement the Boxable interface.

new Box(new MyValue); // This is fine.
new Box('Boxable?');  // This would fail!

The constraint isn't limited to simple names. You can actually constraint a type parameter to any valid PHP type string (unions, intersection types, etc).

Type constraints also change the transpiled code. Since there is something that T must be, the transpiled code can replace all usages of T with the constraint, instead of just leaving the types blank or mixed.

Since type parameters can also be added to interfaces and traits, there needs to be a way to define the type parameters when implementing and using those structures.

Imagine the following interface.

interface Enumerable<T>
{
    public function all(): array<T>;
}

If a Collection class wanted to implement this interface, it needs to tell Enumerable what the T type is.

class UserCollection implements Enumerable<User>
{
    public function all(): array<User>
    {
        // ...
    }
}

Or for a more generic Collection, you could forward a type parameter from the class to the interface.

class Collection<T> implements Enumerable<T>
{
    public function all(): array<T>
    {
        // ...
    }
}

A similar syntax can be used for using generic traits.

class Collection<T>
{
    use EnumeratesValues<T>;
}

This was just a short introduction to the generic syntax that PXP introduces. Once this feature is available, the documentation will contain all of the information that you need to start using them in your own code.