Как стать автором
Обновить

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

Время на прочтение3 мин
Количество просмотров3.8K

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!

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 19: ↑18 и ↓1+17
Комментарии48

Публикации

Истории

Работа

PHP программист
150 вакансий

Ближайшие события