Customizing Keys When Mapping Collections

People often ask me, "how do I specify keys when I'm mapping a collection?"

It actually ends up being a pretty interesting topic, so I decided to cover it in a short screencast, as well as in written format below.

Say you had a list of employees and you needed to transform it into a lookup table mapping their email addresses to their names.

Given this input...

$employees = [
    [
        'name' => 'John',
        'department' => 'Sales',
        'email' => 'john@example.com'
    ],
    [
        'name' => 'Jane',
        'department' => 'Marketing',
        'email' => 'jane@example.com'
    ],
    [
        'name' => 'Dave',
        'department' => 'Marketing',
        'email' => 'dave@example.com'
    ],
];

...we need to create an associative array that looks like this:

$emailLookup = [
    'john@example.com' => 'John',
    'jane@example.com' => 'Jane',
    'dave@example.com' => 'Dave',
];

How can we do this using collections and higher order functions?

Map to Nowhere

Since we know we need to transform each element in the first array into a new element in the associative array, the first thing we might try is to use the map operation:

$emailLookup = $employees->map(function ($employee) {
    return $employee['name'];
});

This does give us a list of employee names, but we have no way of setting the keys!

Back to the drawing board...

PHP's Array Identity Crisis

This problem of wanting to customize keys during a map operation is something I get asked about pretty regularly.

I think the reason it seems like a tricky problem is because in PHP, we use the same data type to represent both a list and a dictionary.

Forget about PHP for a minute and pretend we were trying to solve this problem in JavaScript.

We'd start with a similar array of employee objects:

const employees = [
    {
        'name': 'John',
        'department': 'Sales',
        'email': 'john@example.com'
    },
    {
        'name': 'Jane',
        'department': 'Marketing',
        'email': 'jane@example.com'
    },
    {
        'name': 'Dave',
        'department': 'Marketing',
        'email': 'dave@example.com'
    },
];

...but our desired output would be an object like this:

const emailLookup = {
    'john@example.com': 'John',
    'jane@example.com': 'Jane',
    'dave@example.com': 'Dave',
};

So we wouldn't actually be transforming one array into another array, we would be reducing our initial array into a single object.

Implementing this transformation in JavaScript with reduce would look like this:

const emailLookup = employees.reduce(function (emailLookup, employee) {
    emailLookup[employee.email] = employee.name;
    return emailLookup;
}, {});

Here's the same thing in PHP using Laravel's Collection library:

$emailLookup = $employees->reduce(function ($emailLookup, $employee) {
    $emailLookup[$employee['email']] = $employee['name'];
    return $emailLookup;
}, []);

A Reusable Abstraction

Even though we've solved our original problem, I'm not totally satisfied with how the code reads.

Something I mention in Refactoring to Collections is that I often see reduce as a sign that I'm missing a more expressive abstraction built on top of reduce.

It would be great if we could create an operation like toAssoc() that could use to transform a list into an associative array, but how could we specify both the key and the value?

Learning from Other Languages

The Ruby equivalent of an associative array is a Hash.

You can transform an Enumerable into a Hash by calling the to_h method, as long as the enumerable is made of [key, value] pairs.

That means given an array that looks like this:

employees = [
    ['john@example.com', 'John'],
    ['jane@example.com', 'Jane'],
    ['dave@example.com', 'Dave'],
]

...we can turn that into an email lookup hash by calling to_h:

employees.to_h
# => {
#  'john@example.com' => 'John',
#  'jane@example.com' => 'Jane',
#  'dave@example.com' => 'Dave',
# }

Let's implement this with Laravel's Collection class macros!

The toAssoc Macro

Since Collections are macroable, we can add our implementation of toAssoc directly to the Collection class:

Collection::macro('toAssoc', function () {
    return $this->reduce(function ($assoc, $keyValuePair) {
        list($key, $value) = $keyValuePair;
        $assoc[$key] = $value;
        return $assoc;
    }, new static);
});

Now as long our data is structured as [key, value] pairs, we can use toAssoc to transform it into an associative array:

$emailLookup = collect([
    ['john@example.com', 'John'],
    ['jane@example.com', 'Jane'],
    ['dave@example.com', 'Dave'],
])->toAssoc();
// => [
//  'john@example.com' => 'John',
//  'jane@example.com' => 'Jane',
//  'dave@example.com' => 'Dave',
// ]

So how do we get our original list of employees into a list of [key, value] pairs?

Mapping to Pairs

Map of course! Here's what the whole thing would look like:

$emailLookup = collect([
    [
        'name' => 'John',
        'department' => 'Sales',
        'email' => 'john@example.com'
    ],
    [
        'name' => 'Jane',
        'department' => 'Marketing',
        'email' => 'jane@example.com'
    ],
    [
        'name' => 'Dave',
        'department' => 'Marketing',
        'email' => 'dave@example.com'
    ],
])->map(function ($employee) {
    return [$employee['email'], $employee['name']];    
})->toAssoc();

If we wanted to take it a step further, we could even define a mapToAssoc operation that lets us do the transformation in one step:

Collection::macro('mapToAssoc', function ($callback) {
    return $this->map($callback)->toAssoc();
});

...which we could use like this:

$emailLookup = $employees->mapToAssoc(function ($employee) {
    return [$employee['email'], $employee['name']];    
});

Pretty slick!

Learning More

If you enjoyed this post, you might be interested in the book + video course I recently released.

"Refactoring to Collections" is an in-depth guide to understanding and implementing functional programming ideas in PHP.

It covers fundamental topics like imperative vs. declarative programming, understanding higher order functions, and includes 4 hours of screencasts going over refactorings like the one in this post.

Check out the website to learn more, or subscribe below for a free chapter sample.

Refactoring-to-collections

Enjoyed the post?

I recently released a full book and video course covering other refactorings you can do with collections.

If you'd like to check out a chapter sample, subscribe below and I'll email it to you!

Powered by ConvertKit