Skip to content

Instantly share code, notes, and snippets.

@Swader

Swader/post.md Secret

Created April 23, 2016 16:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Swader/08511dd201a0a7960e031668564f3b4d to your computer and use it in GitHub Desktop.
Save Swader/08511dd201a0a7960e031668564f3b4d to your computer and use it in GitHub Desktop.

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

[author_more]

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment