Tagged Unions в PHP (примерно как в Rust)

от автора

В предыдущей статье я писал про добавление 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!

ссылка на оригинал статьи https://habr.com/ru/post/541670/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *