Skip to content

Latest commit

 

History

History
89 lines (67 loc) · 7.66 KB

functions-and-visibility.md

File metadata and controls

89 lines (67 loc) · 7.66 KB

Функции и области видимости

С функциями дело обстоит несколько сложнее из-за неявного просачивания переменных из внешней области видимости. В PHP это должно делаться явным образом - мы всегда обязаны объявлять переменные замыкания через инструкцию use() примерно следующим образом:

./func-example.png

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

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

Есть кстати и другой интересный выход, правда применимый только для тех функций, которые находятся в скоупе модуля. Вместо того, чтобы передавать переменные через замыкание, можно породить целый класс модуля, а переменные скоупа модуля сделать членами класса. Тогда к ним можно будет обращаться через $this независимо от того, насколько глубоко они вложены внутрь других функциональных вызовов. Кроме того, снимается также и ограничение на их модификацию! Выглядеть это может как-то так:

./nested-func-example.png

Как может быть устроен сбор переменных для передачи в замыкание?

Во-первых, нам нужно понимать, когда мы входим в область другой функции и когда мы выходим из нее. Поскольку вложенность может быть произвольной, нам понадобится стекообразная структура, лучше всего подойдет дерево - поскольку функций в одной и той же области может быть объявлено более одной.

./child-scope.png

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

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

./closure-example.png

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

Вывод типов и обработка стандартных функций

Как мы будем поддерживать функции стандартных объектов? Самое простое - в обработчике типа CallExpression смотреть, является ли вызываемое выражение объектом PropertyAccessExpression и если да - смотреть на его последний элемент, который должен быть в списке поддерживаемых транспилятором стандартных функций.

Но что будет если мы захотим транспилировать код наподобие такого?

let arr = [1, 2, 3];
let arrInc = arr.includes(2);

let str = '12345';
let strInc = str.includes('2');

Беда тут заключается в том, что как Array, так и String содержат метод includes в прототипе, и однозначным образом перевести этот код становится невозможно. Но тут к нам на помощь спешит вывод типов: мы всегда можем узнать тип объекта, у которого мы дергаем includes.

./example-type-inference.png

Соответственно, если это строка, мы переводим вызов в strpos, в ином случае - в in_array. Заметим, что дополнительных проверок мы при этом можем и не делать - мы предполагаем, что исходный код синтаксически корректен и typescript проверил, что у объекта есть метод includes. С учетом предположения, что мы не поддерживаем перевод классов, все выглядит достаточно просто, в ином случае нам понадобилось бы еще проверять, что объект принадлежит к “массивным” типам - это тоже поддерживается в тайпчекере, хоть и немного сложным образом.

Далее: Уничтожение неиспользуемого кода