Skip to content

Latest commit

 

History

History
175 lines (126 loc) · 8.64 KB

2012-12-03-kvc-collection-operators.md

File metadata and controls

175 lines (126 loc) · 8.64 KB
layout title ref category rating published description
post
KVC Collection Operators
Foundation
7.9
true
Rubyists laugh at Objective-C’s bloated syntax. Although we lost a few pounds over the summer with our sleek new object literals, those Red-headed bullies still taunt us with their map one-liners and their fancy Symbol#to_proc. Fortunately, Key-Value Coding has an ace up its sleeves.

Rubyists laugh at Objective-C's bloated syntax.

Although we lost a few pounds over the summer with our sleek new object literals, those Red-headed bullies still taunt us with their map one-liners and their fancy Symbol#to_proc.

Really, a lot of how elegant (or clever) a language is comes down to how well it avoids loops. for, while; even fast enumeration expressions are a drag. No matter how you sugar-coat them, loops will be a block of code that does something that is much simpler to describe in natural language.

"get me the average salary of all of the employees in this array", versus...

double totalSalary = 0.0;
for (Employee *employee in employees) {
  totalSalary += [employee.salary doubleValue];
}
double averageSalary = totalSalary / [employees count];

Meh.

Fortunately, Key-Value Coding gives us a much more concise--almost Ruby-like--way to do this:

[employees valueForKeyPath:@"@avg.salary"];

KVC Collection Operators allows actions to be performed on a collection using key path notation in valueForKeyPath:. Any time you see @ in a key path, it denotes a particular aggregate function whose result can be returned or chained, just like any other key path.

Collection Operators fall into one of three different categories, according to the kind of value they return:

  • Simple Collection Operators return strings, numbers, or dates, depending on the operator.
  • Object Operators return an array.
  • Array and Set Operators return an array or set, depending on the operator.

The best way to understand how these work is to see them in action. Consider a Product class, and a products array with the following data:

@interface Product : NSObject
@property NSString *name;
@property double price;
@property NSDate *launchedOn;
@end

Key-Value Coding automatically boxes and un-boxes scalars into NSNumber or NSValue as necessary to make everything work.

Name Price Launch Date
iPhone 5 $199 September 21, 2012
iPad Mini $329 November 2, 2012
MacBook Pro $1699 June 11, 2012
iMac $1299 November 2, 2012

Simple Collection Operators

  • @count: Returns the number of objects in the collection as an NSNumber.
  • @sum: Converts each object in the collection to a double, computes the sum, and returns the sum as an NSNumber.
  • @avg: Takes the double value of each object in the collection, and returns the average value as an NSNumber.
  • @max: Determines the maximum value using compare:. Objects must support comparison with one another for this to work.
  • @min: Same as @max, but returns the minimum value in the collection.

Example:

[products valueForKeyPath:@"@count"]; // 4
[products valueForKeyPath:@"@sum.price"]; // 3526.00
[products valueForKeyPath:@"@avg.price"]; // 881.50
[products valueForKeyPath:@"@max.price"]; // 1699.00
[products valueForKeyPath:@"@min.launchedOn"]; // June 11, 2012

Pro Tip: To get the aggregate value of an array or set of NSNumbers, you can simply pass self as the key path after the operator, e.g. [@[@(1), @(2), @(3)] valueForKeyPath:@"@max.self"] (/via @davandermobile, citing Objective Sea)

Object Operators

Let's say we have an inventory array, representing the current stock of our local Apple store (which is running low on iPad Mini, and doesn't have the new iMac, which hasn't shipped yet):

NSArray *inventory = @[iPhone5, iPhone5, iPhone5, iPadMini, macBookPro, macBookPro];
  • @unionOfObjects / @distinctUnionOfObjects: Returns an array of the objects in the property specified in the key path to the right of the operator. @distinctUnionOfObjects removes duplicates, whereas @unionOfObjects does not.

Example:

[inventory valueForKeyPath:@"@unionOfObjects.name"]; // "iPhone 5", "iPhone 5", "iPhone 5", "iPad Mini", "MacBook Pro", "MacBook Pro"
[inventory valueForKeyPath:@"@distinctUnionOfObjects.name"]; // "iPhone 5", "iPad Mini", "MacBook Pro"

Array and Set Operators

Array and Set Operators are similar to Object Operators, but they work on collections of NSArray and NSSet.

This would be useful if we were to, for example, compare the inventory of several stores, say appleStoreInventory, (same as in the previous example) and verizonStoreInventory (which sells iPhone 5 and iPad Mini, and has both in stock).

  • @distinctUnionOfArrays / @unionOfArrays: Returns an array containing the combined values of each array in the collection, as specified by the key path to the right of the operator. As you'd expect, the distinct version removes duplicate values.
  • @distinctUnionOfSets: Similar to @distinctUnionOfArrays, but it expects an NSSet containing NSSet objects, and returns an NSSet. Because sets can't contain duplicate values anyway, there is only the distinct operator.

Example:

[@[appleStoreInventory, verizonStoreInventory] valueForKeyPath:@"@distinctUnionOfArrays.name"]; // "iPhone 5", "iPad Mini", "MacBook Pro"

This is Probably a Terrible Idea

Curiously, Apple's documentation on KVC collection operators goes out of its way to make the following point:

Note: It is not currently possible to define your own collection operators.

This makes sense to spell out, since that's what most people are thinking about once they see collection operators for the first time.

However, as it turns out, it is actually possible, with a little help from our friend, objc/runtime.

Guy English has a pretty amazing post wherein he swizzles valueForKeyPath: to parse a custom-defined DSL, which extends the existing offerings to interesting effect:

NSArray *names = [allEmployees valueForKeyPath: @"[collect].{daysOff<10}.name"];

This code would get the names of anyone who has taken fewer than 10 days off (to remind them to take a vacation, no doubt!).

Or, taken to a ridiculous extreme:

NSArray *albumCovers = [records valueForKeyPath:@"[collect].{artist like 'Bon Iver'}.<NSUnarchiveFromDataTransformerName>.albumCoverImageData"];

Eat your heart out, Ruby. This one-liner filters a record collection for artists whose name matches "Bon Iver", and initializes an NSImage from the album cover image data of the matching albums.

Is this a good idea? Probably not. (NSPredicate is rad, and breaking complicated logic up is under-rated)

Is this insanely cool? You bet! This clever example has shown a possible direction for future Objective-C DSLs and meta-programming.


KVC Collection Operators are a must-know for anyone who wants to save a few extra lines of code and look cool in the process.

While scripting languages like Ruby boast considerably more flexibility in its one-liner capability, perhaps we should take a moment to celebrate the restraint built into Objective-C and Collection Operators. After all, Ruby is hella slow, amiright? </troll>