Otávio’s blog

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.

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.