Fun and Functional Programming in PHP with Macros

Christopher Pitt
Christopher Pitt
Share

I was so excited about my previous article about PHP macros, that I thought it would be fun for us to explore the intersection of macros and functional programming. PHP is already full of functions, with object oriented patterns emerging relatively late in its life. Still, PHP’s functions can be cumbersome, especially when combined with variable scope rules… Assembling lego blocks Consider the following ES6 (JavaScript) code:

let languages = [
    {"name": "JavaScript"},
    {"name": "PHP"},
    {"name": "Ruby"},
];


const prefix = "language: ";

console.log(
    languages.map(
        language => prefix + language.name
    )
);
In this example, we see languages defined as a list of programming languages. Each programming language is combined with a constant prefix and the resulting array is logged to the console. This is about as much JavaScript as we’re going to see. If you’d like to learn more ES6, check out the documentation for BabelJS. Compare this to similar PHP code:
$languages = [
    ["name" => "JavaScript"],
    ["name" => "PHP"],
    ["name" => "Ruby"],
];

$prefix = "language: ";

var_dump(
    array_map(function($language) use ($prefix) {
        return $prefix . $language;
    }, $languages);
);
It’s not significantly more code, but it isn’t as clear or idiomatic as the JavaScript alternative. I often miss JavaScript’s expressive, functional syntax, when I’m building PHP things. I want to try and win back some of that expressive syntax!

Getting Started

As in the previous article, we’re going to use Márcio Almada’s Yay library. We can install it with:
composer require yay/yay:*
We’ll put our Yay code (very similar to PHP, but with macro support) in files ending in .yphp
and compile them to regular PHP with:
vendor/bin/yay before.yphp > after.php

A Simple Solution

The first thing I want to try is mapping over a collection of things, with syntax more similar to JavaScript. I want something like:
$list->map(function($item) {
    return strtoupper($item);
});
We can already do that, but it requires creating a special PHP class. We can’t use normal array functions on this special class, so the class also needs to have methods to convert to and from native PHP arrays. AIN'T NOBODY GOT TIME FO' THAT! Instead, let’s make a macro:
macro {
    T_VARIABLE·A->map(···expression)
} >> {
    array_map(···expression, T_VARIABLE·A)
}
This macro will match anything that looks like a variable, followed by ->map(...); and convert it to the array_map(...) alternative. So, if we feed it this code:
$languages = [
    ["name" => "JavaScript"],
    ["name" => "PHP"],
    ["name" => "Ruby"],
];

var_dump(
    $languages->map(function($language) {
        return strtoupper($language["name"]);
    })
);
…it will compile to:
$languages = [
    ["name" => "JavaScript"],
    ["name" => "PHP"],
    ["name" => "Ruby"],
];

var_dump(
    array_map(function($language) {
        return strtoupper($language["name"]);
    }, $languages)
);
If you haven’t already, now is a good time to read the previous article, which explains how this code works. You definitely shouldn’t try the following examples if you’re not comfortable with what you’ve seen so far…

A Complex Solution

We’re off to a good start! There’s still a significant difference between JavaScript and PHP: variable scope. If we wanted to use that prefix, we would have to bind it to the callback given to array_map(...). Let’s work around that. To begin with, we’ll re-define the stringify macro we created last time:
macro {(···expression)
} >> {
    ··stringify(···expression)
}
This will replace →(...) with a string version of whatever is matched between the parens. Then we need to define a pattern for matching arrow function syntax:
macro {
    T_VARIABLE·A->map(
        T_VARIABLE·parameter1
        T_DOUBLE_ARROW·arrow
        ···expression
    )
} >> {
    // ...replacement definition
}
This will match $value => $return. We also need to match variations of this which accept keys:
macro {
    T_VARIABLE·A->map(
        T_VARIABLE·parameter1, T_VARIABLE·parameter2
        T_DOUBLE_ARROW·arrow
        ···expression
    )
} >> {
    // ...replacement definition
}
This will match $key, $value => $return. We could combine these into a single matcher, but that would get complicated pretty quickly. So, I’ve opted for two macros, instead. The next step is to capture the variable context, and the mapping function:
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) . ')
        );
    ')
}
We use the get_defined_vars()
function to store all currently defined variables in an array. The scope of these is the same as that in which eval is executed. We then pass the context as a bound parameter to the closure. We also provide both keys and values for the source list. This is an odd side-effect of how array_map works. We’re kinda depending on the order being the same for the return values of array_keys and array_values, but it’s a pretty safe bet. Finally, we need to work out how to use the context in the call to 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) . ')
        );
    ')
}
We use the extract
function to create local variables for each of the keys and values in the $context array. This means every variable in scope outside of array_map will be available inside as well. We return the value of ···expression, just after re-setting the user-defined value variable. For the key/value variation of this, we need to set both variables. The complete macros, for both variants, are:
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) . ')
        );
    ')
}
These macros will accept the following input:
$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"]
    )
);
… and generate code resembling the following:
$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' . ')
        );
    ')
);
Once again, we can see that the generated code isn’t the prettiest. But it does overcome a rather annoying variable scope limitation, and allows shorthand syntax for the traditional array_map approach. Are you impressed? Disgusted? Let us know in the comments below!

Frequently Asked Questions about Functional Programming in PHP with Macros

What is functional programming in PHP?

Functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids changing-state and mutable data. In PHP, functional programming involves using functions as first-class objects, meaning they can be passed as arguments to other functions, returned as values from other functions, or assigned to variables. This approach promotes writing cleaner and more maintainable code.

How do macros work in PHP?

Macros in PHP are not built-in features like in some other languages. Instead, they are implemented using various techniques such as string substitution, anonymous functions, and magic methods. Macros can be used to create reusable code snippets, automate repetitive tasks, and extend the functionality of PHP.

Can I use macros to supply parameters for function calls in PHP?

Yes, you can use macros to supply parameters for function calls in PHP. This can be achieved by defining a macro that takes a function name and an array of parameters as arguments, and then uses the call_user_func_array function to call the specified function with the provided parameters.

How can I dynamically extend PHP objects using macros?

You can dynamically extend PHP objects using macros by defining a macro that adds new methods to an existing class or object at runtime. This can be done using the __call magic method, which is invoked when invoking inaccessible methods in an object context.

What are the benefits of using functional programming and macros in PHP?

Functional programming and macros in PHP can make your code more concise, readable, and maintainable. They allow you to write code that is easier to test and debug, and they can also improve the performance of your applications by reducing the amount of state that needs to be managed.

Are there any drawbacks to using functional programming and macros in PHP?

While functional programming and macros can provide many benefits, they also have some drawbacks. For example, they can make your code more difficult to understand for developers who are not familiar with these concepts. Also, because macros are not a built-in feature of PHP, they can sometimes lead to unexpected behavior or compatibility issues.

How can I learn more about functional programming and macros in PHP?

There are many resources available online for learning about functional programming and macros in PHP. You can start by reading articles and tutorials on websites like SitePoint, Stack Overflow, and Reddit. You can also find many books and online courses that cover these topics in depth.

Can I use functional programming and macros in PHP for commercial projects?

Yes, you can use functional programming and macros in PHP for commercial projects. However, you should be aware that these techniques may not be suitable for all types of projects, and they require a good understanding of PHP and programming concepts in general.

Are there any libraries or frameworks that support functional programming and macros in PHP?

Yes, there are several libraries and frameworks that support functional programming and macros in PHP. For example, the Laravel framework includes a collection class that provides many functional programming methods, and the Yay library provides a macro system for PHP.

How can I debug macros in PHP?

Debugging macros in PHP can be challenging because they are not a built-in feature of the language. However, you can use various techniques such as logging, tracing, and unit testing to debug your macros. You can also use tools like Xdebug and PhpStorm that provide advanced debugging features for PHP.