Pull to refresh

Сравнение систем типов PHP7 и Hack

Reading time5 min
Views22K
Original author: Dan Miller

Одной из интересных вещей в PHP7, кроме невероятной производительности, является введение скалярного type-hinting'а в сочетании с опциональным «strict» режимом. При чтении RFC я заметил, что PHP код в примерах выглядит очень похожим на Hack. Что если выполнить один и тот же код и в PHP7 и в Hack? Какая разница между ними? Вот что я узнал.

Установка


Получите следующий результат:

$ php --version
PHP 7.0.0-dev (cli) (built: Apr 23 2015 01:12:36) (DEBUG)
Copyright (c) 1997-2015 The PHP Group
Zend Engine v3.0.0-dev, Copyright (c) 1998-2015 Zend Technologies
with Zend OPcache v7.0.6-dev, Copyright (c) 1999-2015, by Zend     Technologies

$ hhvm --version
HipHop VM 3.8.0-dev (rel)
Compiler: heads/master-0-gd71bec94dedc8ca2e722f5619f565a06ef587efc
Repo schema: fa9b8305f616ca35f368f3c24ed30d00563544d1

Для того чтобы не изменяя открывающих тегов в исходных файлах выполнять PHP-код в HHVM, исполняйте hhvm с флагом -vEval.EnableHipHopSyntax=true.

Некоторые примеры


Рассмотрим простой код:

<?php
declare(strict_types=1);

function myLog(string $message): string {
  return $message;
}

function add(int $a, int $b): int {
    myLog($a + $b);
    return $a + $b;
}

$result = add(1, 3);
echo $result;

Его выполнение в PHP7 вернет:

Fatal error: Argument 1 passed to myLog() must be of the type string, integer given, called in /home/vagrant/basic/main.php on line 9 and defined in /home/vagrant/basic/main.php on line 4

Выглядит хорошо! PHP7 правильно говорит, что мы передаем целое число ($a + $b) в функцию, которая ожидает строку, и выдает соответствующее сообщение об ошибке. Посмотрим, что скажет HHVM:

Catchable fatal error: Argument 1 passed to myLog() must be an instance of string, int given in /home/vagrant/basic/main.php on line 6

Появилась пара различий:
  • HHVM называет это «catchable» фатальной ошибкой. Интересно, ведь в RFC сказано, что ошибка фактически должна совпадать с HHVM.
  • HHVM сообщает, что ошибка в строке 6, а PHP, что проблема произошла в строке 9. В подобных случаях я бы предпочел PHP подход, нам показывается и где функция была некорректно вызвана, и где определена.

<?hh
declare(strict_types=1);

function myLog(string $message=null): string {
  if ($message === null) {
    return '';
  } else {
    return $message;
  }
}

echo myLog("Hello world!\n");

echo myLog();

PHP с радостью исполняет код. Hack же возвращает ошибку:

/home/vagrant/nullable/main.php:4:16,21: Please add a ?, this argument can be null (Typing[4065])

Hack не позволяет нам иметь дефолтный аргумент со значением null, т.к. не смешивает понятия «необязательный аргумент» с «обязательный аргументом, который позволяет иметь дефолтное значение» (Подробнее об этом читайте в книге Hack and HHVM). Язык предлагает вам сделать аргумент nullable:

<?hh
declare(strict_types=1);

function myLog(?string $message=null): string {
  if ($message === null) {
    return '';
  } else {
    return $message;
  }
}

echo myLog("Hello world!\n");

echo myLog();

Давайте попробуем что-нибудь посложнее. Что произойдет, если мы смешаем типизации в PHP? Обратите внимание, что определение strict-режима в верхней части файла не имеет никакого эффекта в HHVM.

<?php

function add(int $a, int $b): int {
    myLog($a + $b);
    return $a + $b;
}

<?php
declare(strict_types=1);

function myLog(string $message): string {
  return $message;
}

<?php

require 'add.php';
require 'logger.php';

$result = add(1, 3);
echo $result;

$ php main.php
4
 
$ hhvm -vEval.EnableHipHopSyntax=true main.php
Catchable fatal error: Argument 1 passed to myLog() must be an instance of string, int given in /home/vagrant/separate_files_mixed/lo

Для logger.php включился strict-режим, но PHP позволяет передать int в него из nonstrict-файла. HHVM в подобном случае выбрасывает исключение. Что произойдет, если мы переведем add.php в режим строгой типизации:

Fatal error: Argument 1 passed to myLog() must be of the type string, integer given, called in /home/vagrant/separate_files_mixed/add.php on line 5 and defined in /home/vagrant/separate_files_mixed/logger.php on line 4

Так-то лучше. Strict-режим действует только в тех файлах, где он указан, даже если декларирующий функцию файл подразумевает иное. А что произойдет, если мы вызовем non-strict функцию, из strict-функции? Для реализации я поставил следующие значения для файлов:

logger.php - non-strict
add.php - strict

Fatal error: Argument 1 passed to myLog() must be of the type string, integer given, called in /home/vagrant/separate_files_mixed/add.php on line 5 and defined in /home/vagrant/separate_files_mixed/logger.php on line 3

Получается, что функция является строго типизированной, если она вызывается из функции, которая объявлена в файле с соответствующим заголовком. Впрочем, это влияет только на прямые вызовы. Если мы объявим main.php строго типизированным, PHP радостно вернет нам 4, несмотря на несоответствие типов, которые мы передаем в log().

В Hack соотношение обратное. Если HHVM выполняет main.php в нестрогом режиме, и логгер написан на Hack (c hh тегом в верхней части файла), мы все равно получим ошибку типа несмотря на то, что вызываемый файл не написан на Hack.

Catchable fatal error: Argument 1 passed to myLog() must be an instance of string, int given in /home/vagrant/separate_files_mixed/logger.php on line 5

Другим интересным отличием между системами типов Hack и PHP является аннотация float. Возьмем пример:

<?php
declare(strict_types=1);

function add(float $a, float $b): float {
    return $a + $b;
}

echo add(1, 2);

При выполнении в PHP вернется 3, хотя мы передаем int в том месте, где аннотировали float и несмотря на то, что режим строгой типизации включен. Причина заключается в том, что в PHP7 поддерживается расширяющее примитивное преобразование (Widening primative conversion) при включенном строгом режиме. Это означает, что параметры аннотированные как float могут иметь значение int в тех случаях, когда возможно безопасное преобразование (почти всегда). HHVM не поддерживает подобное поведение и выбрасывает ошибку типов при исполнении приведенного выше кода:

Catchable fatal error: Argument 1 passed to add() must be an instance of float, int given in /home/vagrant/main.php on line 6

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

Заключение


В то время, как Hack поддерживает множество фишек, PHP7 не поддерживает типы nullable, mixed, void возвращаемые значения, коллекции, async и т. д. Но все же меня сильно радует безопасность и читаемость, которая достигается новым strict-режимом в PHP7.

После написания пары небольших проектов на Hack я понял, что не сами типы делают Hack приятным, а та самая обратная связь, которую создает язык между разработчиком и машиной. Наличие интеграции проверки типов для Hack в моем редакторе означает, что вся кодовая база анализируется за доли секунды после того, как я сохраню файл. Сразу же отсекаются глупые или неочевидные ошибки, которые я допустил. Я все чаще стал ловить себя на том, что не задумываюсь о возвращаемых значениях функций, просто пишу код по наитию, предполагая очевидные варианты. Если будет ошибка, редактор немедленно сообщит мне. Пара небольших правок и можно двигаться дальше.

PHP также всегда способствовал тесной обратной связи между машиной и разработчиком. Сохраните файл, обновите страницу в браузере, повторите до достижения результата. Быстро и удобно. Но проверка типов в Hack делает это даже быстрее. Я с нетерпением жду появления аналогичного функционала в IDE/редакторах для строгой типизации в новом PHP.
Tags:
Hubs:
+14
Comments17

Articles