Today I was experimenting with Objective-C's blocks so I thought I'd be clever and add to NSArray a few functional-style collection methods that I've seen in other languages:
@interface NSArray (FunWithBlocks)
- (NSArray *)collect:(id (^)(id obj))block;
- (NSArray *)select:(BOOL (^)(id obj))block;
- (NSArray *)flattenedArray;
@end
The collect: method takes a block which is called for each item in the array and expected to return the results of some operation using that item. The result is the collection of all of those results. (If the block returns nil, nothing is added to the result set.)
The select: method will return a new array with only the items from the original that, when passed as an argument to the block, the block returned YES.
And finally, the flattenedArray method iterates over the array's items. If an item is an array, it recursively calls flattenedArray on it and adds the results to the result set. If the item isn't an array, it adds the item to the result set. The result set is returned when everything is finished.
So now that I had some infrastructure, I needed a test case. I decided to find all package files in the system's application directories. This is what I came up with:
NSArray *packagePaths = [[[NSSearchPathForDirectoriesInDomains(NSAllApplicationsDirectory, NSAllDomainsMask, YES) collect:^(id path) { return (id)[[[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:nil] collect:^(id file) { return (id)[path stringByAppendingPathComponent:file]; }]; }] flattenedArray] select:^(id fullPath) { return [[NSWorkspace sharedWorkspace] isFilePackageAtPath:fullPath]; }];
Yep - that's all one line and it's horrid. I tried a few approaches at adding newlines and indentation to try to clean it up, but it still feels like the actual algorithm is lost in all the noise. I don't know if it's just a syntax thing or my relative in-experience with using a functional style that's the problem, though.
For comparison, I decided to do it "the old fashioned way" and just use loops:
NSMutableArray *packagePaths = [NSMutableArray new];
for (NSString *searchPath in NSSearchPathForDirectoriesInDomains(NSAllApplicationsDirectory, NSAllDomainsMask, YES)) {
for (NSString *file in [[NSFileManager defaultManager] contentsOfDirectoryAtPath:searchPath error:nil]) {
NSString *packagePath = [searchPath stringByAppendingPathComponent:file];
if ([[NSWorkspace sharedWorkspace] isFilePackageAtPath:packagePath]) {
[packagePaths addObject:packagePath];
}
}
}
IMO this version was easier to write and is more readable to boot.
I suppose it's possible this was somehow a bad example, but it seems like a legitimate way to use blocks to me. (Am I wrong?) Am I missing something about how to write or structure Objective-C code with blocks that would clean this up and make it clearer than (or even just as clear as) the looped version?
Blocks are a language-level feature added to C, Objective-C and C++, which allow you to create distinct segments of code that can be passed around to methods or functions as if they were values. Blocks are Objective-C objects, which means they can be added to collections like NSArray or NSDictionary .
__block is a storage qualifier that can be used in two ways: Marks that a variable lives in a storage that is shared between the lexical scope of the original variable and any blocks declared within that scope. And clang will generate a struct to represent this variable, and use this struct by reference(not by value).
Objective-C is general-purpose language that is developed on top of C Programming language by adding features of Small Talk programming language making it an object-oriented language. It is primarily used in developing iOS and Mac OS X operating systems as well as its applications.
Use newlines and break up your call across multiple lines.
The standard pattern used across all of Apple's APIs is that a method or function should only take one block argument and that argument should always be the last argument.
Which you have done. Good.
Now, when writing the code that uses said API, do something like:
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSAllApplicationsDirectory, NSAllDomainsMask, YES);
paths = [paths collect: ^(id path) {
...
}];
paths = [paths collect: ^(id path) {
...
}];
paths = [paths select: ^(id path) {
...
}];
I.e. do each step of your collect/select/filter/flatten/map/whatever as a separate step. This will be no faster/slower than chained method calls.
If you do need to nest blocks in side of blocks, then do so with full indention:
paths = [paths collect: ^(id path) {
...
[someArray select:^(id path) {
...
}];
}];
Just like nested if statements or the like. When it gets too complex, refactor it into functions or methods, as needed.
I think the issue is that (contrary to what critics of Python claim ;) white space matters. In a more functional style, it seems like it would make sense to copy the style of other functional languages. The more LISP-y way to write your example could be something like:
NSArray *packagePaths = [[[NSSearchPathForDirectoriesInDomains(NSAllApplicationsDirectory, NSAllDomainsMask, YES)
collect:^(id path) {
return [[[NSFileManager defaultManager]
contentsOfDirectoryAtPath:path
error:nil]
collect:^(id file) {
return [path stringByAppendingPathComponent:file];
}
];
}
]
flattenedArray
]
select:^(id fullPath) {
return [[NSWorkspace sharedWorkspace] isFilePackageAtPath:fullPath];
}
];
I wouldn't say this is clearer than the looped version. Like any other tool, blocks are a tool and they should be used only when they're the appropriate tool for the job. If readability suffers, I'd say it's not the best tool for the job. Blocks are, afterall, an addition to a fundamentally imperative language. If you really want the conciseness of a functional language, use a functional language.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With