Underscore.m

a functional toolbelt for Objective-C

Underscore.m is a small utility library to facilitate working with common data structures in Objective-C.

It tries to encourage chaining by eschewing the square bracket]]]]]].

It is inspired by the awesome underscore.js. Underscore.m was written by Robert Böhnke and is MIT licensed.

Examples

Consider these Hello World! strings:

NSDictionary *dictionary = @{
  @"en": @"Hello world!",
  @"sv": @"Hej världen!",
  @"de": @"Hallo Welt!",
  @"ja": [NSNull null]
}

Underscore.m makes extracting the strings and capitalizing them a breeze.

NSArray *capitalized = Underscore.dict(dictionary)
  .values
  .filter(Underscore.isString)
  .map(^NSString *(NSString *string) {
    return [string capitalizedString];
  })
  .unwrap;

Underscore.m is especially useful when you deal with structured data from web APIs.

NSArray *tweets = Underscore.array(results)
    // Let's make sure that we only operate on NSDictionaries, you never
    // know with these APIs ;-)
    .filter(Underscore.isDictionary)
    // Remove all tweets that are in English
    .reject(^BOOL (NSDictionary *tweet) {
        return [tweet[@"iso_language_code"] isEqualToString:@"en"];
    })
    // Create a simple string representation for every tweet
    .map(^NSString *(NSDictionary *tweet) {
        NSString *name = tweet[@"from_user_name"];
        NSString *text = tweet[@"text"];

        return [NSString stringWithFormat:@"%@: %@", name, text];
    })
    .unwrap;

Installation

It is recommended to use CocoaPods to install Underscore.m. Alternatively you can download the code from GitHub.

You may want to alias Underscore to _ to make accessing Underscore.m’s methods more concise.

#import "Underscore.h"
#define _ Underscore

NSArray

The following methods can be used with NSArray and NSMutableArray instances. With Underscore.m’s array methods can use a functional-style syntax as well as chaining to create powerful expressions.

Underscore.m supports the following methods to manipulate arrays:

array Underscore.array(NSArray *array)

Wraps an array in an USArrayWrapper. Use this method if you want to chain multiple operations together. You will need to call unwrap to extract the new array at the end.

NSArray *elements = Underscore.array(array)
    .flatten
    .uniq
    .unwrap;

Since array is meant for chaining, you probably don’t need to keep a reference to the USArrayWrapper.

unwrap wrapper.unwrap

Extracts the array of an USArrayWrapper.

first Underscore.first(NSArray *array)

Returns the first element of the array or nil if it is empty.

id first = Underscore.first(array);

last Underscore.last(NSArray *array)

Returns the last element of the array or nil if it is empty.

id last = Underscore.last(array);

head Underscore.head(NSArray *array, NSUInteger n)

Returns the first n elements or all of them, if there are less than n elements in the array.

NSArray *firstThree = Underscore.head(array, 3);

tail Underscore.tail(NSArray *array, NSUInteger n)

Returns the last n elements or all of them, if there are less than n elements in the array.

NSArray *lastThree = Underscore.tail(array, 3);

indexOf Underscore.indexOf(NSArray *array, id obj)

Returns the index of the first occurrence of obj in array or NSNotFound, if the element could not be found.

NSUInteger twentySix = Underscore.indexOf(alphabet, @"z");

flatten Underscore.flatten(NSArray *array)

Recursively flattens the array.

NSArray *arrayOfArrays = @[ @[ @1, @2], @[ @3, @4], @[ @5, @6] ];
NSArray *oneToSix      = Underscore.flatten(arrayOfArrays);

without Underscore.without(NSArray *array, NSArray *values)

Returns all elements not contained in values.

NSArray *oddNumbers = Underscore.without(allNumbers, evenNumbers);

shuffle Underscore.shuffle(NSArray *array)

Shuffles the array using the Fisher-Yates-Shuffle.

NSArray *shuffled = Underscore.shuffle(array);

reduce Underscore.reduce(id memo, UnderscoreReduceBlock block)

reduceRight Underscore.reduceRight(id memo, UnderscoreReduceBlock block)

Reduces the array to a single value using the block.

NSArray *numbers = @[ @1, @2, @3, @4, @5, @6, @7 ];
NSArray *sum     = Underscore.array(numbers)
    .reduce(@0, ^(NSNumber *x, NSNumber *y) {
        return @(x.integerValue + y.integerValue);
    })
    .unwrap;

each wrapper.each(UnderscoreArrayIteratorBlock block)

arrayEach Underscore.arrayEach(NSArray *array, UnderscoreArrayIteratorBlock block)

Calls block once with every member of the array. This method returns the same array again, to facilitate chaining.

Functional syntax:

Underscore.arrayEach(objects, ^(id obj) {
    NSLog(@"%@", obj);
});

Chaining:

Underscore.array(objects)
    .each(^(id obj) {
        NSLog(@"%@", obj);
    });

map wrapper.map(UnderscoreArrayMapBlock block)

arrayMap Underscore.arrayMap(NSArray *array, UnderscoreArrayMapBlock block)

Calls block once with every element of the array. If the block returns nil, the object is removed from the array. Otherwise, the return-value replaces the object.

Functional syntax:

Underscore.arrayMap(strings, ^(NSString *string) {
    return string.capitalizedString;
});

Chaining:

Underscore.array(strings)
    .map(^(NSString *string) {
        return string.capitalizedString;
    });

pluck Underscore.pluck(NSArray *array, NSString *keyPath)

Returns an array containing the objects’ values for the given key path.

NSArray *names = Underscore.pluck(users, @"name");

uniq Underscore.uniq(NSArray *array)

Returns a new array containing all elements of array exactly once.

NSArray *uniques = Underscore.uniq(@[ @1, @1, @2, @3 ]);

find Underscore.find(NSArray *array, UnderscoreTestBlock test)

Returns an object from the array the passes the test or nil, if none of the elements match.

User *admin = Underscore.find(users, ^BOOL (User *user) {
        return user.isAdmin;
    })

filter Underscore.filter(NSArray *array, UnderscoreTestBlock test)

Returns all elements that pass the test.

NSArray *dictionaries = Underscore.filter(objects, Underscore.isDictionary);

reject Underscore.reject(NSArray *array, UnderscoreTestBlock test)

Returns all elements that fail the test.

NSArray *dictionaries = Underscore.reject(objects, Underscore.isNull);

all Underscore.all(NSArray *array, UnderscoreTestBlock test)

Returns YES if all elements pass the test.

BOOL onlyStrings = Underscore.all(objets, Underscore.isString);

any Underscore.any(NSArray *array, UnderscoreTestBlock test)

Returns YES if any of the elements pass the test.

BOOL containsNull = Underscore.any(objets, Underscore.isNull);

NSDictionary

The following methods can be used with NSDictionary and NSMutableDictionary instances. With Underscore.m’s dictionary methods can use a functional-style syntax as well as chaining to create powerful expressions.

Underscore.m supports the following methods to manipulate dictionaries:

dict Underscore.dict(NSDictionary *dictionary)

Wraps a dictionary in an USDictionaryWrapper. Use this method if you want to chain multiple operations together. You will need to call unwrap to extract the result.

NSDictionary *user = Underscore.dict(data)
    .rejectValues(Underscore.isNull)
    .defaults(@{
        @"avatar":          kDefaultAvatar,
        @"backgroundColor": kDefaultBackgroundColor
    })
    .unwrap;

Since dict is meant for chaining, you probably don’t need to keep a reference to the USDictionaryWrapper.

unwrap wrapper.unwrap

Extracts the dictionary of an USDictionaryWrapper.

keys Underscore.keys(NSDictionary *dictionary)

Returns the keys of a dictionary.

NSArray *keys = Underscore.keys(dictionary);

When called on a USDictionaryWrapper, it returns a USArrayWrapper to facilitate chaining.

id key = Underscore.dict(dictionary)
    .keys
    .first;

values Underscore.values(NSDictionary *dictionary)

Returns the values of a dictionary.

NSArray *values = Underscore.values(dictionary);

When called on a USDictionaryWrapper, it returns a USArrayWrapper to facilitate chaining.

id value = Underscore.dict(dictionary)
    .values
    .first;

each wrapper.each(UnderscoreDictionaryIteratorBlock block)

dictEach Underscore.each(NSDictionary *dictionary, UnderscoreDictionaryIteratorBlock block)

Calls block once with every key-value pair of the dictionary. This method returns the same dictionary again, to facilitate chaining.

Functional syntax:

Underscore.dictEach(dictionary, ^(id key, id obj) {
    NSLog(@"%@: %@", key, obj);
});

Chaining:

Underscore.dict(objects)
    .each(^(id key, id obj) {
        NSLog(@"%@: %@", key, obj);
    });

map wrapper.map(UnderscoreDictionaryMapBlock block)

dictMap Underscore.map(NSDictionary *dictionary, UnderscoreDictionaryMapBlock block)

Calls block once with every key-value pair of he dictionary. If the block returns nil, the key-value-pair is removed from the dictionary. Otherwise, the return-value replaces the value.

Functional syntax:

Underscore.dictMap(dictionary, ^(id key, id obj) {
    if ([obj isKindOfClass:NSString.class]) {
        return [(NSString *)obj capitalizedString]
    } else {
        return obj;
    });

Chaining:

Underscore.dict(dictionary)
    .map(^(id key, id obj) {
        if ([obj isKindOfClass:NSString.class]) {
            return [(NSString *)obj capitalizedString]
        } else {
            return obj;
        }
    });

pick Underscore.pick(NSDictionary *dictionary, NSArray *keys)

Returns a copy of dictionary that contains only the keys contained in keys.

NSDictionary *subset = Underscore.pick(info, @[ @"name", @"email", @"address" ]);

extend Underscore.extend(NSDictionary *destination, NSDictionary *source)

Returns a dictionary that contains a union of key-value-pairs of destination and source. Key-value-pairs of source will have precedence over those taken from destination.

NSDictionary *dictionary = Underscore.extend(user, @{ @"age": @50 });

defaults Underscore.defaults(NSDictionary *dictionary, NSDictionary *defaults)

Returns a dictionary that contains a union of key-value-pairs of dictionary and defaults. Key-value-pairs of destination will have precedence over those taken from defaults.

NSDictionary *dictionary = Underscore.defaults(user, @{ @"avatar": kDefaultAvatar });

A common use case for defaults is sanitizing data with known values.

NSDictionary *user = Underscore.dict(data)
    .rejectValues(Underscore.isNull)
    .defaults(@{
        @"avatar":          kDefaultAvatar,
        @"backgroundColor": kDefaultBackgroundColor
    })
    .unwrap;

filterKeys Underscore.filterKeys(NSDictionary *dictionary, UnderscoreTestBlock test)

Returns a dictionary that only contains the key-value-pairs whose keys pass test.

NSDictionary *soundcloudRelated = Underscore.filterKeys(data, ^BOOL (NSString *key) {
    return [key hasPrefix:@"soundcloud-"];
});

filterValues Underscore.filterValues(NSDictionary *dictionary, UnderscoreTestBlock test)

Returns a dictionary that only contains the key-value-pairse whose values pass test.

NSDictionary *numericValues = Underscore.filterValues(data, Underscore.isNumber);

rejectKeys Underscore.rejectKeys(NSDictionary *dictionary, UnderscoreTestBlock test)

Returns a dictionary that only contains the key-value-pairs whose keys fail test.

NSDictionary *safe = Underscore.rejectKeys(data, ^BOOL (NSString *key) {
    return [blackList containsObject:key];
});

rejectValues Underscore.rejectValues(NSDictionary *dictionary, UnderscoreTestBlock test)

Returns a dictionary that only contains the key-value-pairs whose values fail test.

NSDictionary *noNulls = Underscore.rejectValues(data, Underscore.isNull);

Helpers

negate Underscore.negate(UnderscoreTestBlock block)

Returns a block that negates block

id notNull = Underscore.find(array, Underscore.negate(Underscore.isNull));

isEqual Underscore.isEqual(id obj)

Returns a block that returns YES whenever it is called with an object equal to obj.

BOOL containsNeedle = Underscore.any(haystack, Underscore.isEqual(@"needle"));

isArray Underscore.isArray

A block that returns YES if it is called with an NSArray.

isDictionary Underscore.isDictionary

A block that returns YES if it is called with an NSDictionary.

isNull Underscore.isNull

A block that returns YES if it is called with an NSNull.

isNumber Underscore.isNumber

A block that returns YES if it is called with an NSNumber.

isString Underscore.isString

A block that returns YES if it is called with an NSString.