Collection Extensions

Brown is Ugly

Well it’s been a long, long time since I posted anything here. If you’re reading this it’s either because we knock back drinks together at Mac events or you’re someone I should be buying a drink for at a Mac event. The basic idea I initially had for this blog was that I’d post a series of Weird & Wonderful Cocoa tricks. There’s tons of great Cocoa content out there ranging from introductory to pretty advanced but it seemed like there was a hole in the Semi-Abusive segment. I’ve had a number of ideas I’ve wanted to put up but with the pressures of the oppressive corporate world (I work at a subsidiary of an umbrella company held by a Rogue Amoeba interest. We’re hiring.) and various other things on my plate nothing ever gets written. The thing about the Weird & Wonderful is that while it’s easy to know if something is weird it’s something else to know if it is indeed Wonderful and it’s something even more else to be able to explain why it’s Wonderful. So in the interest of trying to keep my feed from turning brown in NetNewsWire I think I’ll start to mix it up a little. That said, let’s get Weird & Wonderful.

Loopy Problems

You’re too smart to be writing yet another loop. Nine times out of ten it’s mostly boiler plate – the operation you’re doing can be easily explained in a couple of words but expressing it in code is cumbersome. Even with Objective-C 2.0′s new for in iteration loops are still basically boring beasts. Let’s see what we can do to avoid them.

At the mega-giant corporation where I slave under the ever watchful eye of a hawkish manager we often need to iterate over Employees to create the endless reports required by our strict oversight committee. We’d like to be able to say, “Give me the names of all Employees who’s computers are idle for more than 5 minutes”. Here’s how we’d do it in Cocoa:

NSMutableArray *namesOfIdleEmployees = [NSMutableArray array];
for ( Employee *employee in allEmployees )
{
    if ( employee.idleTime > 5 )
    {
        [namesOfIdleEmployees addObject: employee.name];
    }
}

Now, sure, that doesn’t look like much code and it’s not really arduous to write but you’ve got better things to do with your time than crank out code like that. Like make more money for your overlords. They love that.

So what are we actually doing in that loop? How would we express it in words? Maybe “collect the names of employees who are idle for more than five minutes”. That sounds like a nice concise order, one you could pretty easily turn into a method on the collection. But if you did that in this case there’s a billion other cases you’d have to do too. And that gets more tiring that just doing the loops when you need them. It would be nice if we could just ask the collection though. Kind of like how we can ask for a @distinctUnionOfObjects. However, to do that we’d need to add our own array operators or override valueForKeyPath: or something weird.

Weird & Wonderful

NSArray *names = [allEmployees valueForKeyPath: @"[collect].{idleTime>5}.name"];

Kind of nifty, isn’t it? What we’re doing here is saying we want to filter based on idle time and any objects that pass the filter we collect their name. Easy as pie.

To do this you do indeed have to override valueForKeyPath: – there’s no way to add your own @style array operators. That’s ok though because once we’ve overridden valueForKeyPath: we can get fancy with how we handle things and add some pretty powerful functionality.

  • method calls

    NSArray *results = [myCollection valueForKeyPath: @"[collect].name"];
    

    Method calls go between []‘s. In this case since there’s no parameter given an implicit parameter is assumed. The result of this expression is basically: [myCollection collect: @"name"] – the collect method iterates over the objects in the collection and gathers all the values for the key ‘name’.

    You can have more complicated method calls:

    NSArray *results = 
    [myCollection valueForKeyPath: @"[collect].name.[componentsSeparatedByString: ' ']"];
    

    The result of this expression is that componentsSeparatedByString: @" " will be called for the value of ‘name’ in each object in the collection. The resulting array of components will then be gathered by the ‘collect’ call. The end result is an array of subarrays containing the words of the name.

  • inline predicates

    NSArray *results = 
    [myCollection valueForKeyPath: @"[collect].{salary>100}.jobTitle"];
    

    Predicates can be specified between {}’s. The predicate string is used to create an NSPredicate which is then used to evaluate the object. If the object matches the predicate then it returns the value of the remainder of the keypath otherwise it returns nil. In this case we use the predicate to filter the collection based on a salary. Each object is checked if it’s salary property is greater than 100. If it is then the value of it’s jobTitle property is returned. If it’s not nil is returned. Collect gathers the results ignoring any that are nil. The end effect of this is that you’ll get an array of all the job titles where salary is more than 100.

  • inline value transformers

     NSArray *results =
     [myCollection valueForKeyPath:
        @"[collect].<NSUnarchiveFromDataTransformerName>.imageData"];
    

    You may specify the name of a value transformer between <>’s. The value transformer is handed the value of the remainder of the keypath. In this case we use the unarchive from data value transformer and hand it some imageData. The result of the transformer is then collected by ‘collect’. The resulting array would contain unarchived NSImage instances. You may specify any of the build in value transformers by their constants or you can use your own value transformers names.

Some Examples

NSArray *waitsAlbumCovers =
[myRecordCollection valueForKeyPath:
@"[collect].{artist like 'Tom Waits'}.<NSUnarchiveFromDataTransformerName>.albumCoverImageData"];

waitsAlbumCovers now contains NSImage instances for each of the albums in my collection where ‘Tom Waits’ is the artist.

NSString *albumsTitles =
[myRecordCollection valueForKeyPath:
@"[concatenate: * withSeparator: ', '].{artist=='Tom Waits'}.albumTitle"];

albumTitles contains a string of all Tom Wait’s album titles separated with ‘, ‘. This example shows the use of a special place holder symbol. The ‘*’ expands during evaluation to the remainder of the keypath. In this case the resulting call on the collection would look like: [myRecordCollection concatentate: @"{artist=='Tom Waits'}.albumTitle" withSeparator: @", "]; The concatenate:withSeparator: method would then iterate over the contents of the collection and concatenate the value of {artist like 'Tom Waits'}.albumTitle placing the separator in between.

Implementation

The code for all this is actually surprisingly small. It’s one file, one class which isn’t actually really used and a couple of categories. Objective-C & Cocoa are just ridiculously awesome at times.

First up we need to override valueForKeyPath:. That’s totally possible to do – we make our own NSObject subclass then inherit everything from that and we’re good to go. Except that’s a lame solution because then we’d not be able to use these calls on stock Cocoa collections. What we want is to replace NSObject‘s implementation with our own. On Leopard there’s a new way of doing that: class_replaceMethod. We use that early in the life time of the process (in main.m before you actually kick off the AppKit) and we’re good to go.

The code for our version of valueForKeyPath: is also pretty basic. We’re given a “key path” which is a string with components separated by ‘.’ characters. We just use componentsSeparatedByString: to grab each component then iterate over each one. If we detect that the component starts with one of our special characters ( [, { or < ) then we handle it. Otherwise we just recurse down the component list.

collect:, concatenate: and other methods need to be implemented by each of the collection classes you'd want to call them on. Rather than code them for each kind of collection I hung them off NSObject in a category and, better, I also made NSObject be able to act as a collection by implementing NSFastEnumeration for it. So now this is valid code:

    NSString *exampleString = @"Example string";
    for ( NSString *string in exampleString ) NSLog( @"%@", string );

That'll print out "Example string". As far as I'm concerned single instances should behave as collections of 1 item. Adding this to NSObject doesn't effect NSArray, NSSet, or NSDictionary enumerations at all. The real benefit of NSObject being a collection is that this, sort of contrived example, works:

    NSString *jobTitle =
        [person valueForKeyPath: @"[collect].{hasNotBeedFiredYet==YES}.jobTitle”];

You'll get nil or the jobTitle depending on the person not being canned yet.

The best way to check out the implementation though is to grab the source code. It should be pointed out that this code should really be taken as a proof of concept. There's so much room for optimization and general clean up it's not even funny. It does, however, work, which is always nice, and it shows off what you can do with this kind of approach. Also as a word of warning: I've yet to use this in any sizable project. It may also be a terribly bad idea for some reason that's escaped me but will no doubt be pointed out in the first comment.

All the warnings out of the way I think what you can do with this stuff is pretty powerful and will save a ton of boring loop iteration code. It lets you more easily express what you want rather than the iterative steps needed to get it done. And that's always a good thing.

One thought on “Collection Extensions”

  1. I was inspired by this marvelous hack (which I use surprisingly often) and wrote a similar one, which you can read about here: http://www.degutis.org/read/9 .. giving this code out is the least I can do to repay this wonderfully generous and brilliant community, and the people who contribute to it! Thanks Guy, you rock!

Comments are closed.