Привет, Хабр! Предлагаю вашему вниманию перевод доклада Александра Кузьменко с прошедшей недавно (14-15 июня) конференции Hong Kong Open Source Conference 2019.
До того, как присоединиться к Haxe Foundation в качестве разработчика компилятора Haxe, Александр около 10 лет профессионально занимался программированием на PHP, так что он знает предмет доклада.
Небольшое введение о том, что такое Haxe — это кроссплатформенный набор инструментов для создания ПО, в состав которого входят:
- компилятор, который в зависимости от целевой платформы либо транслирует исходный Haxe-код в исходный код на других языках программирования (C++, PHP, Java, C#, JavaScript, Python, Lua), либо компилирует непосредственно в байт-код виртуальной машины (JVM, neko, HashLink, Flash)
- стандартная библиотека, реализованная для всех поддерживаемых платформ
- также Haxe предоставляет средства для взаимодействия с кодом, написанным на языке целевой платформы
- стандартный менеджер библиотек — haxelib
Поддержка PHP в Haxe появилась довольно давно — еще в 2008 году. В версиях Haxe до 3.4.7 включительно поддерживался PHP 5.4 и выше, а начиная с четвертой версии в Haxe поддерживается PHP версии 7.0 и выше.
Вы спросите: зачем PHP-разработчику использовать Haxe?
Основная причина для этого — возможность использовать одну и ту же логику и на сервере и на клиенте.
Рассмотрим такой пример: у вас есть сервер, написанный на PHP с использованием фреймворка Laravel, и несколько клиентов, написанных на JavaScript, Java, C#. В таком случае для обработки сетевого взаимодействия между сервером и клиентом (логика которого по идее одна и та же) от вас потребуется написать 4 реализации для каждого из используемых языков. Но с помощью Haxe вы можете написать код сетевого протокола один раз и затем скомпилировать/транслировать его под разные платформы, значительно сэкономив на этом время.
Вот еще пример — игровое приложение — клон Clash of Clans. Александр участвовал в разработке подобной игры: ее сервер был написан на PHP, мобильный клиент — на C# (Xamarin), а браузерный клиент — на JavaScript с использованием фреймворка Phaser. На клиенте и сервере обрабатывалась одна логика — так называемая "боёвка", которая просчитывала поведение юнитов игроков на локации. Изначально код боёвки был написан для каждой из платформ по-отдельности. Но со временем (а проект развивался около 5 лет) накапливались различия в ее поведении на сервере и на клиентах. Связано это было с тем, что писали ее разные люди, каждый из которых реализовывал ее по-своему. Из-за этого не было надежного способа обнаружения читеров, т.к логика на сервере вела себя совсем не так, как на клиенте, в результате страдали и честные игроки, т.к. сервер мог посчитать их читерами и не засчитать результаты честного боя.
В конце концов было принято решение перевести боёвку на Haxe, что позволило полностью решить проблему с читерами, т.к. теперь логика боёвки вела себя одинаково на всех платформах. Кроме того, данное решение позволило сократить расходы на дальнейшее развитие боевой системы, т.к. теперь было достаточно одного программиста, знакомого с Haxe, вместо трех, каждый из которых отвечал бы за свою платформу.
В чем Haxe и PHP отличаются друг от друга? В общем, синтаксис Haxe не покажется PHP-программисту чем-то чужеродным. Да, он отличается, но в большинстве случаев он будет очень похож на PHP.
На слайде показано сравнение кода для вывода строки в консоль. В Haxe код выглядит практически так же, за исключением того, что для работы с функциями PHP необходимо их импортировать (см. первую строку).
А вот так выглядит сгенерированный PHP-код в сравнении с написанным вручную.
Компилятор Haxe автоматически добавляет в код блок комментариев с описанием типов аргументов функций и возвращаемыми типами, поэтому полученный код можно подключить к PHP-проекту и для него прекрасно будет работать автодополнение.
Рассмотрим некоторые существенные отличия в синтаксисе Haxe и PHP.
Начнем с отличий в синтаксисе анонимных функций (на примере их использования для сортировки массива).
На слайде показана анонимная функция, которая захватывает и изменяет значение локальной переменной desc
. В PHP для этого необходимо явно указать, какие переменные доступны в теле анонимной функции. Кроме того, чтобы иметь возможность изменять значение переменной, необходимо добавить &
перед ее именем.
В Haxe такая необходимость отпадает, т.к. компилятор сам определяет, к каким переменным вы обращаетесь. Кроме того, в Haxe 4 появились стрелочные функции (краткая форма описания анонимных функций), используя которые можно сократить наш пример с сортировкой массива всего до одной строки.
Еще одно отличие в синтаксисе — это различия в описании управляющей конструкции switch
. В PHP switch
работает так же как и в Си. В Haxe он работает иначе:
- во-первых, в switch не используется ключевое слово
break
- во-вторых, можно объединять несколько условий с помощью
|
(а не дублировать ключевое словоcase
) - в-третьих, в Haxe для
switch
используется pattern matching (механизм сопоставления с образцом). Так, например, можно применитьswitch
к массиву, и в условиях можно определить действия в зависимости от содержимого массива (знак_
означает, что данное значение нас не волнует и может быть любым). Условие[1, _, 3]
будет выполнено если массив состоит из трех элементов, при этом первый элемент равен 1, третий — 3, а значение второго — любым.
В Haxe все является выражением, и это позволяет писать более компактный код. Можно вернуть значение из try
/catch
, if
или switch
, не используя ключевое слово return
внутри данных конструкций. В приведенном примере с try
/catch
компилятор "знает", что вы хотите вернуть некоторое значение, и сможет передать его из данной конструкции.
PHP постепенно движется в направлении более строгой типизации, но в Haxe уже есть строгая статическая типизация!
В приведенном примере мы присваиваем переменной s
значение, полученное из функции functionReturnsString()
, которая возвращает строку. Таким образом, тип переменной s
— строка. И если попытаться передать ее в функцию giveMeInteger()
, ожидающую в качестве аргумента целое число, то компилятор Haxe выдаст ошибку о несоответствии типов.
Этот пример также демонстрирует еще одну важную особенность Haxe — выведение типов (type inference) — способность компилятора самостоятельно определять типы переменных в зависимости от того, какое значение ей присвоить.
Для программиста это означает, что в большинстве случаев явно указывать типы переменных необязательно. Так в приведенной функции isSmall()
компилятор определит, что тип аргумента a
— целое число, т.к. в первой строке тела функции мы сравниваем значение a
с целым числом. Далее компилятор на основании того, что во второй строке тела функции мы возвращаем true
, определяет, что возвращаемый тип — булева величина. И т.к. компилятор уже определил тип возвращаемого значения, то при дальнейших попытках вернуть из функции какой-либо другой тип, он выдаст ошибку несоответствия типов.
В Haxe, в отличие от PHP, нет автоматического преобразования типов. Например, в PHP возможно вернуть строку из функции, для которой указано, что она возвращает целое число, в таком случае при выполнении скрипта строка будет преобразована в число (не всегда успешно). А в Haxe аналогичный код попросту не скомпилируется — компилятор выдаст ошибку несоответствия типов.
Еще одним отличием Haxe является его расширенная система типов, включающая в себя:
- типы функций, состоящие из типов аргументов функции и возвращаемого типа
- обобщенные (параметризированные) типы (к ним, например, относятся массивы, для которых в качестве параметра используется тип хранимых значений)
- перечисляемые типы
- обобщенные алгебраические типы данных
- типы анонимных структур позволяют объявлять типы для объектов без объявления классов
- абстрактные типы данных (абстракции над существующими типами, но без потери производительности в рантайме)
Все перечисленные типы при компиляции преобразуются в PHP-классы.
Одной из главных отличительных особенностей Haxe является метапрограммирование (в Haxe оно называется макросами), то есть возможность автоматической генерации исходного кода программы.
Макросы выполняются во время компиляции программы и пишутся на обычном Haxe.
Макросы имеют полный доступ к абстрактному синтаксическому дереву, то есть могут читать (искать в нем требуемые выражения) и изменять его.
Макросы могут генерировать выражения, изменять существующие типы, а также создавать новые.
В контексте PHP макросы могут например использоваться для роутинга. Скажем, у вас есть простой класс-роутер, в котором реализованы метод для отображения страницы по ее идентификатору, а также метод для выхода из системы. Используя макрос, можно сгенерировать код для метода route()
, который на основании http-запроса будет перенаправлять его в соответствующий метод класса Router
. Таким образом, отпадает необходимость вручную писать if’ы для вызовов каждого из методов данного класса (макрос сделает это автоматически при компиляции проекта в PHP). Заметьте, что полученный код не использует рефлексию, не требует никаких специальных конфигурационных файлов, или каких-либо еще дополнительных ухищрений, поэтому работать будет он очень быстро.
Еще один пример использования макросов — генерация кода для парсинга и валидации JSON. К примеру, у вас есть класс Data
, объекты которого должны создаваться на основании данных, получаемых из JSON. Это можно сделать с помощью макроса, но так как у макроса есть доступ к структуре класса Data
, то в дополнение к парсингу можно сгенерировать код для валидации JSON, добавив выброс исключений при отсутствии полей или несоответствии типа данных. Таким образом, можно быть уверенным в том, что ваше приложение не пропустит некорректные данные, получаемые от пользователей или от стороннего сервера.
Стоит также упомянуть важные о��обенности реализации некоторых типов данных для платформы PHP, т.к. если не учитывать их, то можно столкнуться с неприятными последствиями.
В PHP строки бинарно-безопасные (binary safe), поэтому в PHP, если вам не требуется поддержка Юникод, методы для работы со строками работают очень быстро.
В Haxe, начиная с четвертой версии, строки поддерживают Юникод, поэтому при их компиляции в PHP будут использоваться методы из модуля для работы с многобайтовыми строками mbstring, а это означает медленный доступ к произвольным символам в строке, медленное вычисление длины строки.
Поэтому если поддержка Юникода вам не нужна, то для работы со строками можно использовать методы класса php.NativeString
, которые будут использовать "родные" строки из PHP.
На слайде слева представлен код на PHP, который использует как методы, поддерживающие Юникод, так и нет.
Справа представлен эквивалентный код на Haxe. Как видно, если вам нужна поддержка Юникод, то необходимо использовать методы класса String
, если нет — то методы класса php.NativeString
.
Еще один важный момент — это работа с массивами.
В PHP массивы передаются по значению, также массивы поддерживают как числовые, так и строковые ключи.
В Haxe массивы передаются по ссылке и поддерживают только числовые ключи (если необходимы строковые ключи, то в Haxe для этого следует использовать класс Map
). Также в Haxe-массивах не допускаются "дыры“ в индексах (индексы должны идти непрерывно).
Также стоит отметить, что запись в массив по индексу в Haxe довольно медленная.
Здесь приведен код для "маппинга" массива с использованием стрелочной функции. Как видно, компилятор Haxe довольно активно оптимизирует PHP-код, получаемый на выходе: анонимная функция в полученном коде отсутствует, вместо этого ее код применяется в цикле к каждому элементу массива. Кроме map()
такая оптимизация применяется и к методу filter()
.
Также при необходимости в Haxe можно использовать класс php.NativeArray
и соответствующие методы PHP для работы с массивами.
Анонимные объекты реализованы с помощью класса HxAnon
, который наследуется от класса StdClass
из PHP.
В PHP StdClass
— это класс, в экземпляры которого превращается всё, что мы пытаемся конвертировать в объект. И он бы идеально подошёл для реализации анонимных объектов, если бы не одна особенность их спецификации: в Haxe обращение к несуществующему полю анонимного объекта должно возвращать null
, а в PHP это выкидывает warning. Из-за этого пришлось отнаследоваться от стандартного класса из PHP и добавить ему магический метод, который при обращении к несуществующим свойствам возвращает null
.
Haxe может взаимодействовать с кодом, написанным на PHP. Для этого имеются следующие возможности (аналогичные возможностям взаимодействия с JavaScript-кодом):
- экстерны (externs)
- вставки PHP-кода напрямую в Haxe-код
- специальные классы из пакета
php.*
php.Syntax
— для специальных конструкций PHP, которых нет в Haxephp.Global
— для "нативных" глобальных функций PHPphp.Const
— для "нативных" глобальных констант PHPphp.SuperGlobal
— для сверхглобальных переменных PHP, доступных отовсюду ($_POST
,$_GET
,$_SERVER
и т.п.)
Т.к. PHP использует классическую ООП-модель, то написание экстернов для него — довольно простой процесс. Фактически экстерны для PHP-классов — это практически дословный "перевод" в Haxe, за исключением некоторых ключевых слов.
В качестве примера так будет выглядеть код экстерна для PHP-класса со слайда выше:
Константы из PHP-класса "превращаются" в статические переменные в Haxe-коде (но с добавлением специальных мета-тэгов).
Статическая переменная $useBuiltinEncoderDecoder
становится статической переменной useBuiltinEncoderDecoder
.
Отсюда видно, что экстерны для PHP-классов можно создавать автоматически (Александр планирует реализовать генератор экстернов в этом году).
Для вставок PHP-кода используется специальный модуль php.Syntax
. Код, добавляемый таким способом, не подвергается никаким преобразованиям и оптимизациям со стороны компилятора Haxe.
Кроме php.Syntax
в Haxe осталась возможность использования untyped-кода.
Также стоит упомянуть такие особенности Haxe, которых нет в PHP:
- реальные свойства с методами для чтения и записи
- поля и переменные, доступные только для чтения
- статические расширения
- мета-тэги, которые можно использовать для аннотации полей и которые доступны для чтения в макросах. Таким образом, мета-тэги используются в основном для метапрограммирования
- Null-безопасность (экспериментальная функция)
- условная компиляция, которая позволяет включать/отключать части кода, которые могут быть доступными, например, в отладочной версии приложения
- компилятор Haxe генерирует высоко-оптимизированный PHP-код, который может работать в разы быстрее PHP-кода, написанного вручную.
Больше информации о Haxe и о его функциях доступно в его официальном руководстве.