Map, Filter, Sort, and Reduce in Objective-C
After practicing Functional Programming in Swift for a few weeks, I decided to try something a little bit different. I decided it was time to experiment with curried functions, map, filter, sort, and reduce in Objective-C.
Swift collections implement map, sort, filter, and reduce, so the first step of my experiment was to reimplement these in an NSArray
category, trying to match the method signatures that Swift implements.
- Map: Given a transform operation, it returns a new
NSArray
where all its elements are transformed according to the operation. The transform operation takes an object and returns another object (which might be of a different type than the original object). - Filter: Given a condition operation, it returns a new
NSArray
where all its elements satisfy the condition. The condition operation takes an object and returns a boolean. - Sort: Given a sort condition, it returns a new
NSArray
where all its elements are sorted according to the condition. The sort condition takes two objects and returns a boolean. - Reduce: Given a combine operation, it returns a recombined object by recursively processing its constituent parts. The combine operation takes the initial state and an object and returns a recombined object, which is of the same type as the initial state.
The NSArray
category ended up with the following interface:
typedef id(^Transform)(id);
typedef BOOL(^Condition)(id);
typedef BOOL(^SortCondition)(id, id);
typedef id(^Combine)(id, id);
@interface NSArray (MFSR)
- (NSArray *)map:(Transform)transform;
- (NSArray *)filter:(Condition)condition;
- (NSArray *)sort:(SortCondition)isOrderedBefore;
- (id)reduce:(id)initial andCombine:(Combine)combine;
@end
To test the interface above and its implementation, I started with an immutable array of movies—James Bond movies—loaded from a plist file.
NSArray<Movie *> *movies = [[NSBundle mainBundle] moviesFromPlist];
Each movie contains a title, the actor’s name who played Bond, and some additional information about the flick. The movie interface is defined as:
@interface Movie : NSObject
@property (nonatomic, readonly) NSString *title;
@property (nonatomic, readonly) NSString *actor;
@property (nonatomic, readonly) NSInteger year;
@property (nonatomic, readonly) CGFloat boxOffice;
@property (nonatomic, readonly) CGFloat budget;
@property (nonatomic, readonly) CGFloat tomatometer;
@end
Sorting the array of movies requires a sort condition that takes two movies and returns a boolean representing the relationship between them. So, for sorting all the British Secret Service agent movies by budget:
BOOL(^byBudget)(Movie *, Movie *) = ^BOOL(Movie *a, Movie *b) {
return a.budget > b.budget;
};
NSArray<Movie *> *moviesByBudget = [movies sort:byBudget];
Simple.
For filtering, the method requires a movie and returns true when the movie matches the condition and false otherwise. Below is a simple way to create an immutable array containing the Sean Connery movies.
BOOL(^isConnery)(Movie *) = ^BOOL(Movie *a) {
return [a.actor isEqual:@"Sean Connery"];
};
NSArray<Movie *> *conneryMovies = [movies filter:isConnery];
But here is the tricky part. To filter movies played by other actors, more blocks like the one above would be required. The functional way to address this is by using curried functions—a popular technique in Swift (and in other Functional Programming languages).
Since filter expects a block that takes a movie and returns a boolean, another function is required, where its output is a function matching this signature.
The new function takes a string (actor’s name) and returns a function that takes a movie and returns a boolean.
typedef BOOL(^FuncMovieToBool)(Movie *);
FuncMovieToBool(^isActor)(NSString *) = ^FuncMovieToBool(NSString *actor) {
return ^BOOL(Movie *movie) {
return [movie.actor isEqual:actor];
};
};
NSArray<Movie *> *actorMovies = [movies filter:isActor(@"Daniel Craig")];
Without any changes to the filter method signature or implementation, it’s possible to filter the array of movies to get a list of movies played by any actor on the big screen.
Finally, it’s possible to combine all these functions to achieve the desired result. Let’s say I want the movies where Pierce Brosnan played James Bond, sorted by ratings and reduced to a list.
NSArray<Movie *> *moviesByRatings
= [[movies filter:isActor(@"Pierce Brosnan")] sort:byRatings];
NSString *description
= [moviesByRatings reduce:@"Brosnan movies sorted by ratings:"
andCombine:^NSString *(NSString *initial, Movie *movie) {
return [NSString stringWithFormat:@"%@\n* %@ (Tomatometer: %@)",
initial,
movie.title,
@(movie.tomatometer)];
}];
Voilà:
Brosnan movies sorted by ratings:
* GoldenEye (Tomatometer: 82)
* Die Another Day (Tomatometer: 57)
* Tomorrow Never Dies (Tomatometer: 57)
* The World Is Not Enough (Tomatometer: 51)
But putting all the fun and excitement aside, would I use these methods in an Objective-C project? Probably not—NSArray
already implements methods for sorting and filtering arrays using NSPredicate
.
I believe that the best way to learn something new is by experimenting and practicing it. And my point here was to experiment and practice map, filter, sort, reduce, and curried functions. Programming requires practice. Practicing these techniques and methods (and reimplementing them) in Objective-C improved the way I use them in Swift.