Дженерики в PHP

PHP
Дженерики в PHP
Перевод статьи «Generics in PHP: The basics»

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

Давайте поговорим о том, что такое дженерики, почему PHP не поддерживает их и что, возможно, нас ждёт в будущем.

У каждого языка программирования есть определённая система типов. У некоторых языков очень строгая реализация, в то время как у других — PHP относится к этой категории — более слабая.

Системы типов используются по разным причинам, самая очевидная из них — проверка типов.

Представим, что у нас есть функция, которая принимает два числа, два целых числа и выполняет некоторую математическую операцию:

1<?php
2 
3function add($a, $b)
4{
5 return $a + $b;
6}

PHP с радостью позволит вам передавать в эту функцию любые данные: числа, строки, логические значения — неважно. PHP изо всех сил постарается преобразовать переменную, когда в этом есть смысл, например, в случае сложения.

1<?php
2 
3add('1', '2'); // 3

Но эти преобразования — жонглирование типами — часто приводят к неожиданным результатам, если не сказать, что к ошибкам и сбоям.

1<?php
2 
3add([], true); // ?

Можно добавить проверку, чтобы функция работала с любыми входными данными:

1<?php
2 
3function add($a, $b)
4{
5 if (!is_int($a) || !is_int($b)) {
6 return null;
7 }
8 
9 return $a + $b;
10}

Или можно использовать встроенные объявления типов PHP:

1<?php
2 
3function add(int $a, int $b): int
4{
5 return $a + $b;
6}

Многие разработчики в сообществе PHP не используют объявления типов, потому что знают, какие входные данные передавать в функцию — в конце концов, они сами её написали.

Однако такие рассуждения быстро рассыпаются: зачастую вы не единственный, кто работает с этим кодом, вы также используете код, который написан другими — подумайте о том, сколько composer-пакетов вы используете. Поэтому, хотя этот пример в отдельности может показаться не таким уж важным, проверка типов действительно пригодится, когда кодовая база начнёт расти.

Кроме того, добавление объявления типов не только защищает от недопустимого состояния, но и разъясняет, какие входные данные от нас, программистов, ожидаются. Часто с типами данных вам не нужно читать внешнюю документацию, потому что многое из того, что делает функция, уже заключено в объявлении типов.

IDE активно помогают в работе: они могут сообщить программисту, какой тип входных данных ожидает функция или какие поля и методы доступны для объекта, который принадлежит к определённому классу. С помощью IDE, написание кода становится более продуктивным, во многом потому, что они могут статически анализировать объявления типов по всей нашей кодовой базе.

С другой стороны, у систем типов свои ограничения. Простой пример — список элементов:

1<?php
2 
3class Collection extends ArrayObject
4{
5 public function offsetGet(mixed $key): mixed
6 { /* … */ }
7 
8 public function filter(Closure $fn): self
9 { /* … */ }
10 
11 public function map(Closure $fn): self
12 { /* … */ }
13}

У коллекции множество методов, которые работают с любыми типами входных данных: цикл, фильтрация, сопоставление — что угодно; коллекции должно быть не важно, имеет ли она дело со строками или целыми числами.

Но давайте посмотрим на это с точки зрения стороннего наблюдателя. Что произойдёт, если мы хотим быть уверены, что одна коллекция содержит только строки, а другая — только объекты User. Коллекцию не волнует тип данных при переборе элементов, но нас волнует. Мы хотим знать, является ли данный элемент в цикле пользователем или строкой — это большая разница. Но без надлежащей информации о типах данных, IDE не сможет эффективно подсказывать нам во время работы.

1<?php
2 
3$users = new Collection();
4 
5// …
6 
7foreach ($users as $user) {
8 $user-> // ?
9}

Мы могли бы создать отдельные реализации для каждой коллекции: одна работает только со строками, а другая — только с объектами User:

1<?php
2 
3class StringCollection extends Collection
4{
5 public function offsetGet(mixed $key): string
6 { /* … */ }
7}
8 
9class UserCollection extends Collection
10{
11 public function offsetGet(mixed $key): User
12 { /* … */ }
13}

Но что, если нам понадобится третья реализация? Четвёртая? Может быть, десятая или двадцатая. Управлять этим кодом станет довольно мучительно.

Вот тут-то и приходят на помощь дженерики.

Чтобы внести ясность: в PHP нет дженериков. То, что я покажу дальше, невозможно в PHP, но это возможно во многих других языках.

Вместо того чтобы создавать отдельную реализацию для каждого возможного типа, многие языки программирования позволяют разработчикам определять "общий" тип классу коллекции:

1<?php
2 
3class Collection<Type> extends ArrayObject
4{
5 public function offsetGet(mixed $key): Type
6 { /* … */ }
7 
8 // …
9}

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

1<?php
2 
3$users = new Collection<User>();
4 
5$slugs = new Collection<string>();

Может показаться мелочью: добавить тип, но это открывает целый мир возможностей.

Теперь IDE знает, какие данные находятся в коллекции, может подсказать нам многое: не добавляем ли мы элемент с неправильным типом; что мы можем делать с элементами при итерации коллекции; передаём ли мы коллекцию в функцию, которая знает, как работать с этими конкретными элементами.

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