Skip to content

Instantly share code, notes, and snippets.

@morrisonlevi
Last active November 14, 2017 14:06
Show Gist options
  • Save morrisonlevi/74ec75a525ab71df0c75c16cd759c701 to your computer and use it in GitHub Desktop.
Save morrisonlevi/74ec75a525ab71df0c75c16cd759c701 to your computer and use it in GitHub Desktop.
Thinking through some issues with adding generic types in PHP.

Let's consider ArrayAccess as a generic type. In order to preseve backwards- compatibility let's default the types to any type. Although I don't like the name mixed this is what documentation uses so I'll use it here:

class ArrayAccess<Element = mixed, Key = mixed> {
    function offsetExists(Key $offset): boolean;
    function offsetGet(Key $offset): Value;
    function offsetSet(?Key $offset, Element $value): void;
    function offsetUnset(Key $offset): void;
}

At first glance this ought to not break existing implementors because the defaults to mixed will allow covariant return types, and all existing code is not permitted to define a Key type (must not have a type declaration).

Will there be an interoperability concern when used as parameters?

function offsetGet(ArrayAccess $obj, $offset) {
    return $obj[$offset];
}

function first(ArrayAccess<int, int> $in) {
    // what are the rules here? I'm not entirely sure what they are but
    // I suspect this ought to error
    return offsetGet($in, 0);
}

This implies the need for generic functions:

function offsetGet<Element,Key>(ArrayAccess<Element,Key> $obj, Key $offset): Element {
    return $obj[$offset];
}

function first<Element>(ArrayAccess<Element, int> $in): Element {
    return offsetGet($in, 0);
}

We could modify many of our built-in functions to be generic but no existing user-defined code would inter-operate with the new generic types.

Instead of defaulting generic parameter types we could instead consider that omitting them means the engine ought not type-check them. This would be much less work for both the php-src authors and our users as well but the impact needs to be explored.

Additionally, there will still be plenty of cases where we won't be type-safe. Consider a generic filter function. How will we check that the filtering function that is passed is compatible?

function filter<Element,Key>(callable $fn, Iterable<Element,Key> $in): Generator<Element,Key> {
    foreach ($in as $key => $value) {
        // need to ensure this supports signature `(Element): mixed`
        // (mixed because of truthy semantics - could require `bool` if desired)
        if ($fn($value)) {
            yield $key => $value;
        }
    }
}

Without typed callables and having only generic types this might lead to creating an interface:

interface FilteringFunction<T> {
    // return truthy value to keep $elment
    function __invoke(T $element): mixed;
}

However, then only objects can be passed and nobody will already implement this interface. It would be a pain and I doubt it would reach wide adoption.

Even if we support typed callable signatures we're still not all the way there. How do we infer types of array literals and do we really want to force anonymous functions to define types to work with typed callable signatures?

function filter<E,K>(callable(E): mixed $fn, Iterable<E,K> $in): Generator<E,K> {
    foreach ($in as $key => $value) {
        if ($fn($value)) {
            yield $key => $value;
        }
    }
}

// How do we infer the array's generic type parameters?
filter(function ($x) { return $x % 2 == 0; }, [0, 1, 2]);

// Do we force this style?
filter(function ($x) { return $x % 2 == 0; }, array<int, int>(0, 1, 2));

// The callable doesn't define parameter or return types; do we really want to
// force them to?
filter(
    function (int $x): bool {
        return $x % 2 == 0;
    },
    array<int,int>(0, 1, 2)
);

In summary generics lead to more reusable and safe code but there are definite usability, interoperability, and complexity concerns.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment