Одной из интересных вещей в 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.

Комментарии (17)


  1. dim_s
    01.06.2015 11:27

    Мне больше понравился подход PHP7, он более практичен. Например взять тот же float, почему нельзя передавать int? Практически все строго-типизированные языки, которые я знаю, позволяют это делать, потому что так удобнее.


    1. BaRoN
      01.06.2015 11:31

      Главное — чтобы в таком случае генерировался Warning про потерю дробной части числа.


      1. nikita2206
        01.06.2015 11:40
        +4

        Там речь про int -> float, это конверсия без потери (в этом и суть widening-а), а float -> int не разрешен


  1. k12th
    01.06.2015 12:01

    function myLog(string $message): string
    

    Неконсистентно как-то опять, почему не
    function myLog($message: string): string // тип всегда через двоеточие после объявления
    

    или
    function string myLog(string $message) // тип всегда перед объявлением через пробел
    


    1. neolink
      01.06.2015 12:08
      +4

      потому что
      function myLog(array $message)
      уже давно работает

      function string myLog(string $message)
      это C какой-то, ну и синтаксический анализ в php традиционно не контекстно зависимый

      в целом: string норм, вопрос только в ":"

      офф ответ в самом rfc: wiki.php.net/rfc/return_types#position_of_type_declaration


      1. k12th
        01.06.2015 12:12

        Ясно, спасибо!


      1. Fesor
        01.06.2015 18:39

        не контекстно зависимый

        Уже контекстно зависимый, правда опять же это было реализовано уже после принятия этого RFC. А выбор был сделан в пользу более читабельного варианта:

        public static static foo() {}
        // vs
        public static foo() : static {}
        


        А с аргументами да, жалко что нужно сохранять обратную совместимость.

        p.s. опоздал…


    1. beono
      01.06.2015 12:10
      +2

      function myLog($message: string): string 
      


      Противоречит старым правилам записи функций и усложняет перевод старого кода.

      function string myLog(string $message)
      


      Получилась Java :) Тут на вкус и цвет. Немного ухудшает читабельность и поиск нужной функции в коде.


      1. k12th
        01.06.2015 12:14

        Спасибо за объяснение. Я на PHP давно не пишу, отсюда дурацкие вопросы.


  1. theRavel
    01.06.2015 14:07

    Я как-то не обращал на это внимание до статьи, а теперь почитал:

    The strict_types directive only affects the specific file it is used in, and does not affect either other files which include the file, nor other files that are included by the file.

    Привет такая же фигня как «use strict» в каждом файле, оох.


    1. pandas
      01.06.2015 14:37

      Grace period. Всё верно. В следующих итерациях типизация вероятно будет строгая по-умолчанию, без обратной (и по всей видимости прямой, при версиях <=5) совместимости. А может быть так и останется, как знать.


      1. iGusev Автор
        01.06.2015 14:57

        Динамическая типизация — это не недосмотр языка, а одна из его концепций.


        1. pandas
          01.06.2015 15:03
          +2

          Спокойно. Динамическая типизация это прекрасно, это значит что можно делать приведение типов в другие формы. А речь идёт о создании сильной и строгой типизации (Strong typing) в PHP. Это более правильная модель работы с той же памятью, и более корректным приведением типов, например.


          1. nikita2206
            01.06.2015 19:49

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


    1. zviryatko
      01.06.2015 22:31

      А фигурные скобки в этом случае не помогут?

      <?php
      declare(strict_types=1) {
        require "file.php";
      }
      


      1. Fesor
        01.06.2015 23:03

        1) нет никаких фигурных скобок. Обсуждалось в RFC
        2) strict_types действует только на тот файл, в котором оно объявлено и не дальше. То есть на инклуды не распространяется.

        На самом деле можно просто жахнуть правило в PHP-cs-fixer например.


    1. beono
      02.06.2015 13:26
      +1

      Тоже самое с неймспейсами в php и это логичное поведение.
      В данном случае можно просто добавить объявление режима в шаблон создания нового класса в IDE