Я был так взволнован своей предыдущей статьей о макросах PHP , что подумал, что нам будет интересно изучить пересечение макросов и функционального программирования.
PHP уже полон функций, а объектно-ориентированные шаблоны появляются сравнительно поздно. Тем не менее, функции PHP могут быть громоздкими, особенно в сочетании с правилами переменной области …
Рассмотрим следующий код ES6 (JavaScript):
let languages = [ { "name" : "JavaScript" } , { "name" : "PHP" } , { "name" : "Ruby" } , ] ; const prefix = "language: " ; console . log ( languages . map ( language = > prefix + language . name ) ) ;
В этом примере мы видим languages
определенные как список языков программирования. Каждый язык программирования объединяется с постоянным префиксом, а полученный массив записывается на консоль.
Это примерно столько же JavaScript, сколько мы увидим. Если вы хотите узнать больше о ES6, ознакомьтесь с документацией по BabelJS .
Сравните это с похожим кодом PHP:
$languages = [ [ "name" = > "JavaScript" ] , [ "name" = > "PHP" ] , [ "name" = > "Ruby" ] , ] ; $prefix = "language: " ; var_dump ( array_map ( function ( $language ) use ( $prefix ) { return $prefix . $language ; } , $languages ) ; ) ;
Это не намного больше кода, но он не так понятен или идиоматичен, как альтернатива JavaScript. Я часто скучаю по выразительному, функциональному синтаксису JavaScript, когда я создаю PHP-вещи. Я хочу попробовать отыграть этот выразительный синтаксис!
Начиная
Как и в предыдущей статье, мы собираемся использовать библиотеку Yay Марсио Алмады . Мы можем установить его с:
composer require yay / yay : *
Мы поместим наш код Yay (очень похожий на PHP, но с поддержкой макросов) в файлы, заканчивающиеся на .yphp
и скомпилируем их в обычный PHP с помощью:
vendor / bin / yay before . yphp > after . php
Простое решение
Первое, что я хочу попробовать, это сопоставить коллекцию вещей с синтаксисом, более похожим на JavaScript. Я хочу что-то вроде:
$list - > map ( function ( $item ) { return strtoupper ( $item ) ; } ) ;
Мы уже можем это сделать, но это требует создания специального класса PHP. Мы не можем использовать обычные функции массива в этом специальном классе, поэтому у класса также должны быть методы для преобразования в и из собственных массивов PHP.
Вместо этого давайте сделаем макрос:
macro { T_VARIABLE ·A - > map ( ···expression ) } > > { array_map ( ···expression , T_VARIABLE ·A ) }
Этот макрос будет соответствовать всему, что выглядит как переменная, за которой следует ->map(...)
; и преобразовать его в array_map(...)
. Итак, если мы передадим это код:
$languages = [ [ "name" = > "JavaScript" ] , [ "name" = > "PHP" ] , [ "name" = > "Ruby" ] , ] ; var_dump ( $languages - > map ( function ( $language ) { return strtoupper ( $language [ "name" ] ) ; } ) ) ;
… он будет компилироваться в:
$languages = [ [ "name" = > "JavaScript" ] , [ "name" = > "PHP" ] , [ "name" = > "Ruby" ] , ] ; var_dump ( array_map ( function ( $language ) { return strtoupper ( $language [ "name" ] ) ; } , $languages ) ) ;
Если вы еще этого не сделали, сейчас самое время прочитать предыдущую статью , в которой объясняется, как работает этот код. Вы определенно не должны пробовать следующие примеры, если вам не нравится то, что вы видели до сих пор …
Комплексное решение
Мы хорошо начали! Между JavaScript и PHP все еще есть существенная разница: переменная область действия. Если бы мы хотели использовать этот префикс, мы должны были бы связать его с обратным вызовом, данным для array_map(...)
. Давайте обойдем это.
Для начала мы переопределим макрос stringify, который мы создали в прошлый раз:
macro { → ( ···expression ) } > > { ·· stringify ( ···expression ) }
Это заменит →(...)
строковой версией того, что совпадает между скобками. Затем нам нужно определить шаблон для соответствия синтаксису функции стрелки:
macro { T_VARIABLE ·A - > map ( T_VARIABLE ·parameter1 T_DOUBLE_ARROW ·arrow ···expression ) } > > { // ...replacement definition }
Это будет соответствовать $value => $return
. Нам также нужно сопоставить варианты этого, которые принимают ключи:
macro { T_VARIABLE ·A - > map ( T_VARIABLE ·parameter1 , T_VARIABLE ·parameter2 T_DOUBLE_ARROW ·arrow ···expression ) } > > { // ...replacement definition }
Это будет соответствовать $key, $value => $return
. Мы могли бы объединить их в одно совпадение, но это довольно быстро усложнилось бы. Итак, вместо этого я выбрал два макроса.
Следующим шагом является захват переменной контекста и функции отображения:
macro { // ...capture pattern } > > { eval ( ' $context = get_defined_vars ( ) ; return array_map ( function ( $key , $value ) use ( $context ) { // ...give context to map function } , array_keys ( ' . →(T_VARIABLE·A) . ' ) , array_values ( ' . →(T_VARIABLE·A) . ' ) ) ; ' ) }
Мы используем get_defined_vars()
для хранения всех определенных на данный момент переменных в массиве. Область их действия такая же, как и в которой выполняется eval
. Затем мы передаем контекст как связанный параметр в замыкание. Мы также предоставляем ключи и значения для списка источников.
Это странный побочный эффект от работы array_map
. Мы в array_keys
array_values
зависимы от порядка одинаковости возвращаемых значений array_keys
и array_values
, но это довольно безопасная ставка.
Наконец, нам нужно выяснить, как использовать контекст в вызове array_map
:
macro { // ...capture pattern } > > { eval ( ' $context = get_defined_vars ( ) ; return array_map ( function ( $key , $value ) use ( $context ) { extract ( $context ) ; ' . →(T_VARIABLE·parameter1) . ' = $value ; return ( ' . →(···expression) . ' ) ; } , array_keys ( ' . →(T_VARIABLE·A) . ' ) , array_values ( ' . →(T_VARIABLE·A) . ' ) ) ; ' ) }
Мы используем функцию extract
для создания локальных переменных для каждого из ключей и значений в массиве $context
. Это означает, что каждая переменная в области видимости вне array_map
также будет доступна внутри. Мы возвращаем значение ···expression
сразу после переустановки пользовательской переменной значения.
Для этого варианта ключ / значение нам нужно установить обе переменные. Полные макросы для обоих вариантов:
macro { T_VARIABLE ·A - > map ( T_VARIABLE ·parameter1 T_DOUBLE_ARROW ·arrow ···expression ) } > > { eval ( ' $context = get_defined_vars ( ) ; return array_map ( function ( $key , $value ) use ( $context ) { extract ( $context ) ; ' . →(T_VARIABLE·parameter1) . ' = $value ; return ( ' . →(···expression) . ' ) ; } , array_keys ( ' . →(T_VARIABLE·A) . ' ) , array_values ( ' . →(T_VARIABLE·A) . ' ) ) ; ' ) } macro { T_VARIABLE ·A - > map ( T_VARIABLE ·parameter1 , T_VARIABLE ·parameter2 T_DOUBLE_ARROW ·arrow ···expression ) } > > { eval ( ' $context = get_defined_vars ( ) ; return array_map ( function ( $key , $value ) use ( $context ) { extract ( $context ) ; ' . →(T_VARIABLE·parameter1) . ' = $key ; ' . →(T_VARIABLE·parameter2) . ' = $value ; return ( ' . →(···expression) . ' ) ; } , array_keys ( ' . →(T_VARIABLE·A) . ' ) , array_values ( ' . →(T_VARIABLE·A) . ' ) ) ; ' ) }
Эти макросы будут принимать следующие данные:
$languages = [ [ "name" = > "JavaScript" ] , [ "name" = > "PHP" ] , [ "name" = > "Ruby" ] , ] ; $prefix = "language: " ; var_dump ( $languages - > map ( $language = > $prefix . $language [ "name" ] ) ) ; var_dump ( $languages - > map ( $key , $value = > ( $key + 1 ) . ": " . $value [ "name" ] ) ) ;
… и сгенерировать код, похожий на следующий:
$languages = [ [ "name" = > "JavaScript" ] , [ "name" = > "PHP" ] , [ "name" = > "Ruby" ] , ] ; $prefix = "language: " ; var_dump ( eval ( ' $context = get_defined_vars ( ) ; return array_map ( function ( $key , $value ) use ( $context ) { extract ( $context ) ; ' . ' $language ' . ' = $value ; return ( ' . ' $prefix . $language [ "name" ] ' . ' ) ; } , array_keys ( ' . ' $languages ' . ' ) , array_values ( ' . ' $languages ' . ' ) ) ; ' ) ) ; var_dump ( eval ( ' $context = get_defined_vars ( ) ; return array_map ( function ( $key , $value ) use ( $context ) { extract ( $context ) ; ' . ' $key ' . ' = $key ; ' . ' $value ' . ' = $value ; return ( ' . ' ( $key + 1 ) . ": " . $value [ "name" ] . ' . ' ) ; } , array_keys ( ' . ' $languages ' . ' ) , array_values ( ' . ' $languages ' . ' ) ) ; ' ) ) ;
Еще раз, мы видим, что сгенерированный код не самый красивый. Но он преодолевает довольно раздражающее ограничение области видимости и позволяет использовать сокращенный синтаксис для традиционного подхода array_map
.
Вы впечатлены? Недовольный? Дайте нам знать в комментариях ниже!