PHP
Article

Transducers in PHP Made Easy

By Younes Rafie

Have you heard of functional programming, high order functions, etc. before? Probably, right? However, when you hear “transducers”, do you know what those are?

Input/output vector illustration

The Definition of Transducers

We can’t define transducers without talking about reducers first. Quoting Rich Hickey:

A reducing function is just the kind of function you’d pass to reduce – it takes a result so far and a new input and returns the next result-so-far.

A transducer is a function that takes one reducing function and returns another.

Transducers were first introduced into Clojure by Rich Hickey, and ported to PHP by Michael Dowling. Transducers are a powerful way to build algorithmic transformations that you can reuse in many contexts. In this article, we’re going to take a look at how they could be useful through a set of practical examples.

Examples

We need to install the Transducers package via Composer before going any further.

composer require mtdowling/transducers

We’ll use a simple User class for the following examples.

class User
{
    public $id;
    public $name;
    public $age;

    public function __construct($id, $name, $age)
    {
        $this->id = $id;
        $this->name = $name;
        $this->age = $age;
    }

    public function __toString()
    {
        return sprintf("\n%d - %s - %d", $this->id, $this->name, $this->age);
    }
}

// demo data
$data = [
    new User(1, "younes", 24),
    new User(2, "youssef", 26),
    new User(3, "hamza", 25),
    new User(4, "ismail", 17),
];
use Transducers as t;

$uppercase = t\map(function($user) { 
    return new User($user->id, ucfirst($user->name), $user->age); 
});

$result = t\xform($data, $uppercase);

var_dump($result);

The map function is similar to the array_map PHP function: we pass a callable which, in this case, will uppercase the first letter of the user name.

We use the xform function to apply our uppercase transducer. It takes our data for the first parameter and a transducer for the second.

// output
array(4) {
  [0]=>
  object(User)#14 (3) {
    ["id"]=>
    int(1)
    ["name"]=>
    string(6) "Younes"
    ["age"]=>
    int(24)
  }
  [1]=>
  object(User)#15 (3) {
    ["id"]=>
    int(2)
    ["name"]=>
    string(7) "Youssef"
    ["age"]=>
    int(26)
  }
  [2]=>
  object(User)#16 (3) {
    ["id"]=>
    int(3)
    ["name"]=>
    string(5) "Hamza"
    ["age"]=>
    int(25)
  }
  [3]=>
  object(User)#17 (3) {
    ["id"]=>
    int(4)
    ["name"]=>
    string(6) "Ismail"
    ["age"]=>
    int(17)
  }
}

xform returns the same type as the data parameter (array in this case). We can also use to_array if you strictly want to output an array.

// ...
$result = t\to_array($data, $uppercase);
// ...

We can use to_string as well, to convert the output to a string, or into($target, $coll, callable $xf) to convert the output to a specific type. Check the documentation for more details.

use Transducers as t;

$uppercase = t\map(function($user) { 
    return new User($user->id, ucfirst($user->name), $user->age); 
});

$result = t\to_string($data, $uppercase);

var_dump($result);
// output
string(64) "
1 - Younes - 24
2 - Youssef - 26
3 - Hamza - 25
4 - Ismail - 17"

The best part about Transducers is that we can compose multiple transformations into a single transducer. For instance, let’s uppercase the first letter of the user name and remove minors.

$uppercase = t\map(function($user) { 
    return new User($user->id, ucfirst($user->name), $user->age); 
});
$removeMinors = t\filter(function($user) { 
    return $user->age >= 18;
});

$comp = t\comp(
    $uppercase,
    $removeMinors
);

$result = t\to_string($data, $comp);

var_dump($result);

The filter function is similar to the array_filter PHP function. The comp function creates a transducer from a list of transducers, in this case uppercase (using map) and removeMinors (using filter).

// output
string(48) "
1 - Younes - 24
2 - Youssef - 26
3 - Hamza - 25"

Now we have a reusable transducer composition that we can use whenever we want to reduce our data using this criteria. Check out the documentation for the list of available reducing functions.

Creating a Transducer

A reducing function takes a value as a parameter and returns a reducing function array, which must contain three elements:

  • init: A function that returns an initial value for the transducer. It’s only called at first if no initial value is provided.
  • result: The result function is called to build the final result from the call stack.
  • step: This is where you write your reduction logic – you may call it zero or many times depending on your reducer logic.

This becomes really confusing without showing some actual code, so let’s use the take transducer function as an example. It takes n items from the top of the data array.

// ....
$comp = t\comp(
    $uppercase,
    $removeMinors,
    t\take(2)
);

$result = t\to_string($data, $comp);

var_dump($result);
// output
string(33) "
1 - Younes - 24
2 - Youssef - 26"

Here is the take reducer function’s source code.

function take($n)
{
    return function (array $xf) use ($n) {
        $remaining = $n;
        return [
            'init'   => $xf['init'],
            'result' => $xf['result'],
            'step'   => function ($r, $input) use (&$remaining, $xf) {
                $r = $xf['step']($r, $input);
                return --$remaining > 0 ? $r : ensure_reduced($r);
            }
        ];
    };
}

The take function is being called several times with the result and the input parameters. On every call, it decrements the remaining variable and tests if it’s less than zero. In that case, we return a Reduced object instance, which indicates a stopping point.

Our transducer function example will drop null elements from the data. Using the previous explanation of how transducers work, we can access the $input variable, and decide whether to call the next step callback or simply return the value.

function dropNull()
{
    return function (array $xf) {
        return [
            'init'   => $xf['init'],
            'result' => $xf['result'],
            'step'   => function ($result, $input) use ($xf) {
                return $input === null
                    ? $result
                    : $xf['step']($result, $input);
            }
        ];
    };
}

We can test this by adding some null items to our $data variable.

$data = [
    null,
    new User(1, "younes", 24),
    new User(2, "youssef", 26),
    new User(3, "hamza", 25),
    new User(4, "ismail", 17),
    null
];
$result = t\to_string($data, t\dropNull());

var_dump($result);
// output
string(64) "
1 - younes - 24
2 - youssef - 26
3 - hamza - 25
4 - ismail - 17"

Conclusion

In this article, we got acquainted with a new aspect of the functional programing world called transducers. We’ve gone over the purpose of transducers, which is to make the transformation of data easier. We also went over some examples to better demonstrate the value of transducers. You now have a new tool in your developer belt or, at least, a better understanding of the transducer concept.

If you have any questions about transducers, you can post them below!

  • M S i N Lund

    Seems to add a whole lot of complexity, and makes the code very difficult to wrap your head around.

    I like to be able to see what the code does by looking at it.

    Is there anything you can do with this, that you couldn’t do without it?

  • http://exclusive-paper.com JanySmool

    Hello Younes, thank you for this explanetion! It was very interesting for me)) for this transducers I must use only PHP?

    • younesrafie

      Transducers exist on other languages too, but we used the PHP transducer package here

  • http://pqr7.wordpress.com Petr Myazin

    Why you create new User inside map function? Why not just mutate and return existing variable $user?

    • Yoni L.

      Obvously it’s for example purpose, the main goal of the map is to transform to something else for example if you have a typeUser in the user object which is is set to merchant for example, you may want to return a Merchant object in your map function which is a specific user. Thanks for sharinig this!! I used them in javascript with MongoDB but never in PHP!!

    • Bolla Sándor

      In functional programming it’s very common that data passed to functions are immutable so the result would contain new or cloned items of the same elements

  • younesrafie

    For example:
    – You can’t create transducer composition!
    – You don’t iterate on your data multiple time when you’re using multiple reducing function!

  • Dušan Kasan

    What are the advantages of using transducers instead of collection pipeline object? I mean, i get why you would need them in pure functional language but isn’t some Collection object better in OOP languages?

    • Ilya.Savinkov

      Exactly! That was my first thought. Collections and pipelining look much prettier as for me than these unobvious transducers.
      There should be some particular task where transducers fit best. Otherwise they are useless in php.

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in PHP, once a week, for free.