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.