Haxe и PHP: статическая типизация, стрелочные функции и метапрограммирование

До того, как присоединиться к Haxe Foundation в качестве разработчика компилятора Haxe, Александр около 10 лет профессионально занимался программированием на PHP, так что он знает предмет доклада.

image

Небольшое введение о том, что такое Haxe — это кроссплатформенный набор инструментов для создания ПО, в состав которого входят:

  • компилятор, который в зависимости от целевой платформы либо транслирует исходный Haxe-код в исходный код на других языках программирования (C++, PHP, Java, C#, JavaScript, Python, Lua), либо компилирует непосредственно в байт-код виртуальной машины (JVM, neko, HashLink, Flash)
  • стандартная библиотека, реализованная для всех поддерживаемых платформ
  • также Haxe предоставляет средства для взаимодействия с кодом, написанным на языке целевой платформы
  • стандартный менеджер библиотек — haxelib

image

Поддержка PHP в Haxe появилась довольно давно — еще в 2008 году. В версиях Haxe до 3.4.7 включительно поддерживался PHP 5.4 и выше, а начиная с четвертой версии в Haxe поддерживается PHP версии 7.0 и выше.

image

Вы спросите: зачем PHP-разработчику использовать Haxe?
Основная причина для этого — возможность использовать одну и ту же логику и на сервере и на клиенте.
Рассмотрим такой пример: у вас есть сервер, написанный на PHP с использованием фреймворка Laravel, и несколько клиентов, написанных на JavaScript, Java, C#. В таком случае для обработки сетевого взаимодействия между сервером и клиентом (логика которого по идее одна и та же) от вас потребуется написать 4 реализации для каждого из используемых языков. Но с помощью Haxe вы можете написать код сетевого протокола один раз и затем скомпилировать/транслировать его под разные платформы, значительно сэкономив на этом время.

image

Вот еще пример — игровое приложение — клон Clash of Clans. Александр участвовал в разработке подобной игры: ее сервер был написан на PHP, мобильный клиент — на C# (Xamarin), а браузерный клиент — на JavaScript с использованием фреймворка Phaser. На клиенте и сервере обрабатывалась одна логика — так называемая «боёвка», которая просчитывала поведение юнитов игроков на локации. Изначально код боёвки был написан для каждой из платформ по-отдельности. Но со временем (а проект развивался около 5 лет) накапливались различия в ее поведении на сервере и на клиентах. Связано это было с тем, что писали ее разные люди, каждый из которых реализовывал ее по-своему. Из-за этого не было надежного способа обнаружения читеров, т.к логика на сервере вела себя совсем не так, как на клиенте, в результате страдали и честные игроки, т.к. сервер мог посчитать их читерами и не засчитать результаты честного боя.
В конце концов было принято решение перевести боёвку на Haxe, что позволило полностью решить проблему с читерами, т.к. теперь логика боёвки вела себя одинаково на всех платформах. Кроме того, данное решение позволило сократить расходы на дальнейшее развитие боевой системы, т.к. теперь было достаточно одного программиста, знакомого с Haxe, вместо трех, каждый из которых отвечал бы за свою платформу.

image

В чем Haxe и PHP отличаются друг от друга? В общем, синтаксис Haxe не покажется PHP-программисту чем-то чужеродным. Да, он отличается, но в большинстве случаев он будет очень похож на PHP.
На слайде показано сравнение кода для вывода строки в консоль. В Haxe код выглядит практически так же, за исключением того, что для работы с функциями PHP необходимо их импортировать (см. первую строку).

image

А вот так выглядит сгенерированный PHP-код в сравнении с написанным вручную.
Компилятор Haxe автоматически добавляет в код блок комментариев с описанием типов аргументов функций и возвращаемыми типами, поэтому полученный код можно подключить к PHP-проекту и для него прекрасно будет работать автодополнение.

Рассмотрим некоторые существенные отличия в синтаксисе Haxe и PHP.

image

Начнем с отличий в синтаксисе анонимных функций (на примере их использования для сортировки массива).
На слайде показана анонимная функция, которая захватывает и изменяет значение локальной переменной desc. В PHP для этого необходимо явно указать, какие переменные доступны в теле анонимной функции. Кроме того, чтобы иметь возможность изменять значение переменной, необходимо добавить & перед ее именем.
В Haxe такая необходимость отпадает, т.к. компилятор сам определяет, к каким переменным вы обращаетесь. Кроме того, в Haxe 4 появились стрелочные функции (краткая форма описания анонимных функций), используя которые можно сократить наш пример с сортировкой массива всего до одной строки.

image

Еще одно отличие в синтаксисе — это различия в описании управляющей конструкции switch. В PHP switch работает так же как и в Си. В Haxe он работает иначе:

  • во-первых, в switch не используется ключевое слово break
  • во-вторых, можно объединять несколько условий с помощью | (а не дублировать ключевое слово case)
  • в-третьих, в Haxe для switch используется pattern matching (механизм сопоставления с образцом). Так, например, можно применить switch к массиву, и в условиях можно определить действия в зависимости от содержимого массива (знак _ означает, что данное значение нас не волнует и может быть любым). Условие [1, _, 3] будет выполнено если массив состоит из трех элементов, при этом первый элемент равен 1, третий — 3, а значение второго — любым.

image

В Haxe все является выражением, и это позволяет писать более компактный код. Можно вернуть значение из try/catch, if или switch, не используя ключевое слово return внутри данных конструкций. В приведенном примере с try/catch компилятор «знает», что вы хотите вернуть некоторое значение, и сможет передать его из данной конструкции.

image

PHP постепенно движется в направлении более строгой типизации, но в Haxe уже есть строгая статическая типизация!
В приведенном примере мы присваиваем переменной s значение, полученное из функции functionReturnsString(), которая возвращает строку. Таким образом, тип переменной s — строка. И если попытаться передать ее в функцию giveMeInteger(), ожидающую в качестве аргумента целое число, то компилятор Haxe выдаст ошибку о несоответствии типов.
Этот пример также демонстрирует еще одну важную особенность Haxe — выведение типов (type inference) — способность компилятора самостоятельно определять типы переменных в зависимости от того, какое значение ей присвоить.

image

Для программиста это означает, что в большинстве случаев явно указывать типы переменных необязательно. Так в приведенной функции isSmall() компилятор определит, что тип аргумента a — целое число, т.к. в первой строке тела функции мы сравниваем значение a с целым числом. Далее компилятор на основании того, что во второй строке тела функции мы возвращаем true, определяет, что возвращаемый тип — булева величина. И т.к. компилятор уже определил тип возвращаемого значения, то при дальнейших попытках вернуть из функции какой-либо другой тип, он выдаст ошибку несоответствия типов.

image

В Haxe, в отличие от PHP, нет автоматического преобразования типов. Например, в PHP возможно вернуть строку из функции, для которой указано, что она возвращает целое число, в таком случае при выполнении скрипта строка будет преобразована в число (не всегда успешно). А в Haxe аналогичный код попросту не скомпилируется — компилятор выдаст ошибку несоответствия типов.

image

Еще одним отличием Haxe является его расширенная система типов, включающая в себя:

  • типы функций, состоящие из типов аргументов функции и возвращаемого типа
  • обобщенные (параметризированные) типы (к ним, например, относятся массивы, для которых в качестве параметра используется тип хранимых значений)
  • перечисляемые типы
  • обобщенные алгебраические типы данных
  • типы анонимных структур позволяют объявлять типы для объектов без объявления классов
  • абстрактные типы данных (абстракции над существующими типами, но без потери производительности в рантайме)

Все перечисленные типы при компиляции преобразуются в PHP-классы.

image

Одной из главных отличительных особенностей Haxe является метапрограммирование (в Haxe оно называется макросами), то есть возможность автоматической генерации исходного кода программы.
Макросы выполняются во время компиляции программы и пишутся на обычном Haxe.
Макросы имеют полный доступ к абстрактному синтаксическому дереву, то есть могут читать (искать в нем требуемые выражения) и изменять его.
Макросы могут генерировать выражения, изменять существующие типы, а также создавать новые.

image

В контексте PHP макросы могут например использоваться для роутинга. Скажем, у вас есть простой класс-роутер, в котором реализованы метод для отображения страницы по ее идентификатору, а также метод для выхода из системы. Используя макрос, можно сгенерировать код для метода route(), который на основании http-запроса будет перенаправлять его в соответствующий метод класса Router. Таким образом, отпадает необходимость вручную писать if’ы для вызовов каждого из методов данного класса (макрос сделает это автоматически при компиляции проекта в PHP). Заметьте, что полученный код не использует рефлексию, не требует никаких специальных конфигурационных файлов, или каких-либо еще дополнительных ухищрений, поэтому работать будет он очень быстро.

image

Еще один пример использования макросов — генерация кода для парсинга и валидации JSON. К примеру, у вас есть класс Data, объекты которого должны создаваться на основании данных, получаемых из JSON. Это можно сделать с помощью макроса, но так как у макроса есть доступ к структуре класса Data, то в дополнение к парсингу можно сгенерировать код для валидации JSON, добавив выброс исключений при отсутствии полей или несоответствии типа данных. Таким образом, можно быть уверенным в том, что ваше приложение не пропустит некорректные данные, получаемые от пользователей или от стороннего сервера.

Стоит также упомянуть важные особенности реализации некоторых типов данных для платформы PHP, т.к. если не учитывать их, то можно столкнуться с неприятными последствиями.

image

В PHP строки бинарно-безопасные (binary safe), поэтому в PHP, если вам не требуется поддержка Юникод, методы для работы со строками работают очень быстро.
В Haxe, начиная с четвертой версии, строки поддерживают Юникод, поэтому при их компиляции в PHP будут использоваться методы из модуля для работы с многобайтовыми строками mbstring, а это означает медленный доступ к произвольным символам в строке, медленное вычисление длины строки.
Поэтому если поддержка Юникода вам не нужна, то для работы со строками можно использовать методы класса php.NativeString, которые будут использовать «родные» строки из PHP.

image

На слайде слева представлен код на PHP, который использует как методы, поддерживающие Юникод, так и нет.
Справа представлен эквивалентный код на Haxe. Как видно, если вам нужна поддержка Юникод, то необходимо использовать методы класса String, если нет — то методы класса php.NativeString.

image

Еще один важный момент — это работа с массивами.
В PHP массивы передаются по значению, также массивы поддерживают как числовые, так и строковые ключи.
В Haxe массивы передаются по ссылке и поддерживают только числовые ключи (если необходимы строковые ключи, то в Haxe для этого следует использовать класс Map). Также в Haxe-массивах не допускаются «дыры“ в индексах (индексы должны идти непрерывно).
Также стоит отметить, что запись в массив по индексу в Haxe довольно медленная.

image

Здесь приведен код для «маппинга» массива с использованием стрелочной функции. Как видно, компилятор Haxe довольно активно оптимизирует PHP-код, получаемый на выходе: анонимная функция в полученном коде отсутствует, вместо этого ее код применяется в цикле к каждому элементу массива. Кроме map() такая оптимизация применяется и к методу filter().
Также при необходимости в Haxe можно использовать класс php.NativeArray и соответствующие методы PHP для работы с массивами.

image

Анонимные объекты реализованы с помощью класса HxAnon, который наследуется от класса StdClassиз PHP.

image

В PHP StdClass — это класс, в экземпляры которого превращается всё, что мы пытаемся конвертировать в объект. И он бы идеально подошёл для реализации анонимных объектов, если бы не одна особенность их спецификации: в Haxe обращение к несуществующему полю анонимного объекта должно возвращать null, а в PHP это выкидывает warning. Из-за этого пришлось отнаследоваться от стандартного класса из PHP и добавить ему магический метод, который при обращении к несуществующим свойствам возвращает null.

image

Haxe может взаимодействовать с кодом, написанным на PHP. Для этого имеются следующие возможности (аналогичные возможностям взаимодействия с JavaScript-кодом):

  • экстерны (externs)
  • вставки PHP-кода напрямую в Haxe-код
  • специальные классы из пакета php.*
    • php.Syntax — для специальных конструкций PHP, которых нет в Haxe
    • php.Global — для «нативных» глобальных функций PHP
    • php.Const — для «нативных» глобальных констант PHP
    • php.SuperGlobal — для сверхглобальных переменных PHP, доступных отовсюду ($_POST, $_GET, $_SERVER и т.п.)

image

Т.к. PHP использует классическую ООП-модель, то написание экстернов для него — довольно простой процесс. Фактически экстерны для PHP-классов — это практически дословный «перевод» в Haxe, за исключением некоторых ключевых слов.
В качестве примера так будет выглядеть код экстерна для PHP-класса со слайда выше:

image

Константы из PHP-класса «превращаются» в статические переменные в Haxe-коде (но с добавлением специальных мета-тэгов).
Статическая переменная $useBuiltinEncoderDecoder становится статической переменной useBuiltinEncoderDecoder.
Отсюда видно, что экстерны для PHP-классов можно создавать автоматически (Александр планирует реализовать генератор экстернов в этом году).

image

Для вставок PHP-кода используется специальный модуль php.Syntax. Код, добавляемый таким способом, не подвергается никаким преобразованиям и оптимизациям со стороны компилятора Haxe.
Кроме php.Syntax в Haxe осталась возможность использования untyped-кода.
image

Также стоит упомянуть такие особенности Haxe, которых нет в PHP:

  • реальные свойства с методами для чтения и записи
  • поля и переменные, доступные только для чтения
  • статические расширения
  • мета-тэги, которые можно использовать для аннотации полей и которые доступны для чтения в макросах. Таким образом, мета-тэги используются в основном для метапрограммирования
  • Null-безопасность (экспериментальная функция)
  • условная компиляция, которая позволяет включать/отключать части кода, которые могут быть доступными, например, в отладочной версии приложения
  • компилятор Haxe генерирует высоко-оптимизированный PHP-код, который может работать в разы быстрее PHP-кода, написанного вручную.

Больше информации о Haxe и о его функциях доступно в его официальном руководстве.