Статьи

Веселое и функциональное программирование в PHP с макросами

Я был так взволнован своей предыдущей статьей о макросах 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 .

Вы впечатлены? Недовольный? Дайте нам знать в комментариях ниже!