UPD. Во избежание путаницы по просьбам трудящихся хочу еще раз уточнить, что это только черновик, который еще пока что не был официально предложен
В предыдущей статье я писал про добавление enums в PHP8.1. Голосование прошло успешно, так что можно считать, что вопрос решенный.
Однако та реализация enums — лишь часть глобального плана. Сегодня мы рассмотрим следующий пункт, tagged unions, по-русски это переводится как "тип-сумма".
Голосования по нему пока не проходило, но предлагается также включить его в PHP 8.1.
Все эти термины "алгебраические типы данных", "тип-сумма" звучат страшно, но на деле всё довольно просто.
Зачем всё это вообще нужно?
Result как в Расте
Если вы писали на языке Rust, то наверняка встречали встроенный enum Result. В Rust, Go и т.д. нет механизма exception, так как в этих языках считается, что явная обработка ошибок гораздо надёжнее. Язык вынуждает тебя явно проработать все варианты событий, а не кидать исключение в надежде, что кто-то наверху знает о нём и умеет правильно обрабатывать. (Не будем здесь холиварить, на тему exceptions vs return type, у каждого своё мнение). Если говорить конкретно про Раст, то результатом вызова функции, которая может породить ошибку, часто делают значение типа Result.
Result состоит из двух вариантов (case-ов в терминологии enum в PHP): Ok и Err. Варианты мы могли бы сделать и с помощью предыдущего функционала enum, или даже констант, но нам нужно возвращать еще и сами значения. Причем, в случае успеха значением может быть строка, а в случае ошибки какой-нибудь другой тип. Например, integer (статус HTTP-ответа).
Как это будет выглядеть в PHP, если голосование будет успешным:
enum Result {
case Ok(public string $json);
case Err(public int $httpStatus);
}
function requestApi($url): Result {
//
}
теперь мы этот ответ можем передать куда-то еще, и знания об ошибке и ее типе никогда не пропадут.
Как я писал в предыдущей статье, enum — это по сути класс, в нём могут быть методы и т.д. В случае тип-суммы методы могут быть как общими на весь enum, так и на конкретный case.
Вот пример реализации монады Maybe (пример из RFC):
Монада Maybe
(В Расте такой тип называется Option)
enum Maybe {
// This is a Unit Case.
case None {
public function bind(callable $f)
{
return $this;
}
};
// This is a Tagged Case.
case Some(private mixed $value) {
// Note that the return type can be the Enum itself, thus restricting the return
// value to one of the enumerated types.
public function bind(callable $f): Maybe
{
// $f is supposed to return a Maybe itself.
return $f($this->value);
}
};
// This method is available on both None and Some.
public function value(): mixed {
if ($this instanceof None) {
throw new Exception();
}
return $this->val;
}
}
Как вы видите, в этом енаме два варианта: Some и None, причем Some имеет привязанное значение, а None — просто None. У каждого из вариантов может быть своя реализация метода bind. И также общий метод value()
В RFC не описано, но вызываться методы должны примерно также
$a = Maybe::Some("blabla");
// или $a = Maybe::None
$a->bind();
Естественно, просто передавать туда-сюда Result или Maybe смысла нет, нужно где-то всё же обработать это дело. Для этого хорошо подойдет pattern matching, на который уже есть RFC, но он сыроватый. Если включить фантазию, то выглядеть это будет примерно так:
$result = requestApi($url);
if ($result is Result::Some {%$json}) {
// выводим $json
}
if ($result is Result::Err {%$httpStatus}) {
// выводим $httpStatus
}
или примерно тоже самое, но с оператором match.
Еще пример
Есть куча вещей, которые будет удобнее программировать с tagged unions. Например, если кто-то писал парсер, то знает, что первым этапом идет tokenizer (scanner), который разбивает исходный код на токены. Вот тут прямо очень хорошо ложится: есть некий ограниченный набор видов токенов, который можно поместить в enum. Причем некоторые токены, например, строковый литерал или идентификатор, будут содержать еще и значение. В коде будет всё очень удобно и наглядно. Примерно так:
enum Token {
case Comma;
case LeftBrace;
case RightBrace;
case StringLiteral(public string $str);
case Identifier(public string $identifier);
// и т.д.
}
Что дальше?
Неизвестно, примут этот RFC на голосовании или нет, возможно посчитают, что это переусложнит язык и не нужно. Однако тенденция на усиление типизации, а также на перенятие синтаксических "фишек" из других языков налицо. Если примут tagged unions, то я готов поспорить, примут и pattern matching.
Если вам интересны подобные статьи про разработку, в частности, что будет дальше с паттерн матчингом, подписывайтесь на телеграм-канал Cross Join!