Это небольшой разбор поста от PHP Foundation: Compile time generics: yay or nay?, пропитанный личным мнением.

Я сторонник того, что пыхе родные дженерики не очень то и нужны.
Джентльменских дженериков вполне хватает.
Мы, джентльмены, верим друг другу на слово: написан дженерик в аннотации — прекрасно! Стат. анализ рассудит.


Однако команда PHP Foundation проделала колоссальную работу в вопросе добавления дженериков в PHP и сейчас хочет получить обратную связь от сообщества, прежде чем вливать огромные ресурсы в продолжение темы.


Знаю и понимаю, что некоторым из нас хочется дженериков настолько, что без разницы, как именно, ведь “что-то” уже лучше, чем “ничего”.

image.png

Но давайте попробуем не выкрикивать сразу “ДА!” а сначала разберёмся.

Мономорфизация

Мы привыкли к дженерикам в формате коллекции: есть интерфейс Collection<T>; создаём коллекцию типа Collection<User> и понимаем, что из этой коллекции кроме User нам ничего не вывалится. Для нас, разработчиков, Collection<User> — что-то на уровне метаданных. А что под капотом?


Мономорфизация — это техника компиляции дженериков, при которой создаётся отдельная специализированная версия класса (или функции) для каждого конкретного типа, с которым дженерик используется.
Применительно к нашей коллекции это означает, что в рантайме будут существовать конкретные типы Collection<User>, Collection<Post> и так далее для каждого варианта использования коллекции.


В RFC на текущем этапе заявлена только “ручная мономорфизация”:
разработчику необходимо создавать отдельный класс на каждый подтип коллекции;
записи вида $users = new Collection<User>(); пока не поддерживаются.

interface Collection<T> {}

abstract class BaseCollection<T: Entity> {}

final class Users implements BaseCollection<User> {}
final class Posts implements BaseCollection<Post> {}
final class Comments implements BaseCollection<Comment> {}

Кажется, мы и сейчас примерно так можем делать ?

Однако, в данном случае мы получаем проверку типов во всех методах с T, ведь PHP будет при компиляции заменять T в сигнатурах методов на соответствующий класс.

Я почему-то уверен, что никто из нас не захочет заморачиваться и создавать отдельный класс только для того, чтобы добавить типов и обмазаться настоящими дженериками из ядра.

Вопросы и ответы

Дженерики в параметрах

Уже возможно, но в простом варианте (без Union Types):

class DataProcessor
{
    public function __construct(private Repository<UserEntity> $repo) {}
}

Вариантность

Базово дженерики подразумеваются инвариантными. Для ковариантности и контрвариантности рассматривается синтаксис из Kotlin и C#:

interface EventProcessor<in Event, out Result, Context>
{
   public function process(Event $event): Result;
   public function updateContext(Context $context): void;
   public function getCurrentContext(): Context;
}

Вывод типов

class Car<Driver>
{
    public function __construct(private Driver $driver) {}
}

// PHP понимает, что $car имеет тип Car<StudentDriver>
$car = new Car(new StudentDriver());

Это сложно и не вписывается в концепт, ведь требует дженерики в рантайме.

Трейты

Как трейты будут в это вписываться пока не ясно. Скорее всего как-то так:

trait Tools<T>
{
    public function useful(T $param): int { ... }
}

class C
{
    use Tools<Book>;
}

однако, подводных камней много.

Перечисления (enum)

Про них опять забыли или просто не написали. А это, поверьте, отдельная головная боль.

Union Types

Можно забыть про Union внутри дженерика Collection<User|Post>, если нам важна производительность. Но когда дженерик — часть составного типа Repository<UserEntity>|null — в будущем возможно.

iterable<T>

Этот вид дженериков относится к дженерикам рантайма.

Функции

Любители фасадов сразу захотят дженерики в функции collect(). Что-ж, теоретически что-то такое должно работать:

function collect<T>(T ...$items): Collection<T> { /* ... */ }

$users = collect<User>(User::fetchAll());

Вложенные дженерики

В статье отсутствует упоминание типов вида Repository<Collection<BlogPost>> и ограничения глубины вложенности.

На самом деле это интересный вопрос, когда мы говорим о мономорфизации, особенно ручной.

Eval

Ограничения на генерирование дженериков на лету не упоминаются. Вероятно, с этим проблем не будет.

Я уверен, что дай эту имплементацию сообществу, и в Доктрину тут же добавят генерацию классов GenericCollection под каждую ToMany связь. Больше применять этот подход, вроде, негде.

Когда?

Реализация первой части концепта возможна к версии PHP 8.6.

Мои выводы

Compile-Time дженерики не влияют на производительность в рантайме (почти). Но что, если они добавляют сложности?
Без нормального вывода типов дженерики могут стать необходимым злом и дополнительной когнитивной нагрузкой для новичков и тех, кому они не упёрлись.

Просто представьте, что весь вендор обмазан дженериками. Разработчику теперь необходимо их заполнять в своём коде; либо нам делают вывод типов в рантайме и прощай производительность.
Или переложим эту ответственность на IDE?

Джентльменские же дженерики не приносят вреда тем, кто не в клубе джентльменов и не пользуется стат. анализом.

Мне кажется, что этот концепт не выйдет из эксперименталки: уж больно много минусов. Но если выйдет, то хотелось бы иметь возможность переключать через что-то вроде declare(generics=1) в стартовом скрипте. Стираемые или отключаемые дженерики — подходящая опция для меня.


Если принимать такой RFC в работу, то только после полного понимания всей дорожной карты: как мы придём к полноценным дженерикам, которые не будут мешать и будут помогать.
Добавление подобного рода функционала — путь в один конец. Назад будет уже не вертануть.


И самое главное в таком деле — не допускать разработчиков Symfony до ядра PHP. Хватило уже сбрасываемых readonly свойств.

Другие мои статьи, не вписывающиеся в формат хабра, можно найти здесь или в Телеграм-канале PHP Fart Time.


Также вам может быть интересно:


Сейчас compile-time дженерики находятся в экспериментальной стадии и работы еще немерено.
А вы проголосовали бы за то, чтобы втащить такие дженерики?

Комментарии (3)


  1. eee
    05.08.2025 10:15

    По мне так дженериков через аннотации хватает, разве что можно добавить native-аннотации типа #[Template] #[Extends] #[Implements], чтобы IDE-шкам легче было индексировать.


  1. tkutru
    05.08.2025 10:15

    PHP Compile Time Generics: да или нет?

    Нет.


  1. ForsakenROX
    05.08.2025 10:15

    А какие ещё могут быть дженерики ? в первую очередь нужно думать о бенефитах которые это всё предоставляет, а исходя из прочтения оригинальных статей я их для себя так и не смог вывести. Возможно сообществу стоило сместить акцент именно на это, например тотальная типизация могла бы быть использована для кодогенерации в какой нибудь Си. Но пока что это выглядит как тотальное усложнение без видимых преимуществ (о чем критики и говорят упоминая стат анализаторы которые не дают импакта на производительность). Надо продолжать работать, например над библиотеками для ML о чем Пронский и переживает за уход разрабов из стэка в питонисты например. Только тогда комьюнити будет расти и набираться свежей крови.

    Пока что я против, по крайней мере до момента когда не появится вменяемый роадмэп который наглядно объяснит где это можно использовать и принесёт преимущества