views:

1546

answers:

3

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?

A: 

I think the issue is that (contrary to what critics of Python claim ;) white space maters. 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.

Barry Wark
+7  A: 

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.

bbum
Nice solution, though I'm not a big fan of redefining `paths` several times. I'd probably end up naming several values based on their content, and end up with one called `paths`. Just my two cents.
Jonathan Sterling
A: 

Looks like you're re-inventing High-Order Messaging. Marcel Weiher has done extensive work on HOM in Objective-C, which you can find here:

http://www.metaobject.com/blog/labels/Higher%20Order%20Messaging.html

NSResponder
Nope-- HOM is a very different beast than this. May certainly be of interest, though.
bbum