Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there an Objective-C algorithm like `transform` of the C++ STL?

My goal is to have an array that contains all filenames of a specific extension, but without the extension.

There's an elegant solution to get all filenames of a specific extension using a predicate filter and instructions on how to split a path into filename and extension, but to combine them I would have to write a loop (not terrible, but not elegant either).

Is there a way with Objective-C (may be similar to the predicate mechanism) to apply some function to every element of an array and put the results in a second array, like the transform algorithm of the C++ STL does?

What I'd like to write:

// let's pretend 'anArray' was filled by querying the filesystem and not hardcoded
NSArray* anArray = [[NSArray alloc] initWithObjects:@"one.ext", @"two.ext", nil];

// that's what I liked to write (pseudo code)
NSArray* transformed = [anArray transform: stringByDeletingPathExtension];

// Yuji's answer below proposes this (which may be as close as you can get
// to my wish with Objective C)
NSArray* transformed = [anArray my_arrayByApplyingBlock:^(id x){
                           return [x stringByDeletingPathExtension];
                       }];
like image 728
pesche Avatar asked Jan 02 '11 15:01

pesche


2 Answers

Actually, there is a very simple way. It's been around since 2003 and it is poorly named.

NSArray *array = [NSArray arrayWithObjects:@"one.ext", @"two.ext", nil];

// short solution
NSArray *transformed = [array valueForKey:@"stringByDeletingPathExtension"];

// long solution (more robust against refactoring)
NSString *key = NSStringFromSelector(@selector(stringByDeletingPathExtension));
NSArray *transformed = [array valueForKey:key];

Both produce the output:

(
    one,
    two
)
like image 197
Neal Ehardt Avatar answered Oct 21 '22 06:10

Neal Ehardt


That's a topic called Higher Order Messaging in Cocoa, and developed by many people on the web. Start from here and try googling more. They add a category method to NSArray so that you can do

NSArray*transformed=[[anArray map] stringByDeletingPathExtension];

The idea is as follows:

  • [anArray map] creates a temporary object (say hom)
  • hom receives the message stringByDeletingPathExtension
  • hom re-sends the message to all the elements of anArray
  • hom collects the results and returns the resulting array.

If you just want a quick transform, I would define a category method:

@interface NSArray (myTransformingAddition)
-(NSArray*)my_arrayByApplyingBlock:(id(^)(id))block;
@end

@implementation NSArray (myTransformingAddition)
-(NSArray*)my_arrayByApplyingBlock:(id(^)(id))block{
     NSMutableArray*result=[NSMutableArray array];
     for(id x in self){
          [result addObject:block(x)];
     }
     return result;
}
@end

Then you can do

NSArray* transformed=[anArray my_arrayByApplyingBlock:^id(id x){return [x stringByDeletingPathExtension];}];

Note the construct ^ return-type (arguments) { ...} which creates a block. The return-type can be omitted, and clang is quite smart on guessing it, but gcc is quite strict about it and needs to be specified sometime. (In this case, it's guessed from the return statement which has [x stringBy...] which returns an NSString*. So GCC guesses the return type of the block to be NSString* instead of id, which GCC thinks is incompatible, thus comes the error. )

On OS X Leopard or iOS 3, you can use PLBlocks to support blocks. My personal subjective opinion is that people who care about new software typically upgrade to the newest OS, so supporting the latest OS should be just fine; supporting an older OS won't increase your customer by a factor of two...

THAT SAID, there's already a nice open-source framework which does all I said above. See the discussion here, and especially the FunctionalKit linked there.


More addition: it's in fact easy to realize your pseudocode [array transform:stringByDeletingPathExtension].

@interface NSArray (myTransformingAddition)
-(NSArray*)my_transformUsingSelector:(SEL)sel;
@end

@implementation NSArray (myTransformingAddition)
-(NSArray*)my_transformUsingSelector:(SEL)sel;{
     NSMutableArray*result=[NSMutableArray array];
     for(id x in self){
          [result addObject:[x performSelector:sel withObject:nil]];
     }
     return result;
}
@end

Then you can use it as follows:

NSArray*transformed=[array my_transformUsingSelector:@selector(stringByDeletingPathExtension)];

However I don't like it so much; you need to have a method already defined on the object in the array to use this method. For example, if NSString doesn't have the operation what you want to do as a method, what would you do in this case? You need to first add it to NSString via a category:

@interface NSString (myHack)
-(NSString*)my_NiceTransformation;
@end

@implementation NSString (myHack)
-(NSString*)my_NiceTransformation{
   ... computes the return value from self ...
   return something;
}
@end

Then you can use

NSArray*transformed=[array my_transformUsingSelector:@selector(my_NiceTransformation)];

But it tends to be very verbose, because you need to define the method in other places first. I prefer providing what I want to operate directly at the call site, as in

NSArray*transformed=[array my_arrayByApplyingBlock:^id(id x){
   ... computes the return value from x ...
   return something;    
}];

Finally, never add category methods which do not start with a prefix like my_ or whatever. For example, in the future Apple might provide a nice method called transform which does exactly what you want. But if you have a method called transform in the category already, that will lead to an undefined behavior. In fact, it can happen that there is a private method by Apple already in the class.

like image 42
Yuji Avatar answered Oct 21 '22 05:10

Yuji