9

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];
                       }];
Community
  • 1
  • 1
pesche
  • 3,054
  • 4
  • 34
  • 35
  • 2
    Considering all `transform` really does is loop over the elements, I'm not really seeing the inelegance of doing so yourself... – Nicholas Knight Jan 02 '11 at 15:52
  • do you want to do it in-place, or create a new array? Do you also need to remove paths to the files? Sample data (showing the initial array elements and what you want the results to be) would be helpful. The best thing when writing a question is to provide a breadth of information while keeping it concise. – outis Jan 02 '11 at 16:26
  • 1
    @Nicholas: it's about higher abstraction levels and maintainability. When you can express your intention on one line and don't have to write (or copy/paste) the same loop multiple times, the code is easier to understand and maintain. If the single line is sufficiently advanced (a.k.a. too clever for the average programmer), then I'd prefer to loop, too. – pesche Jan 02 '11 at 22:02

2 Answers2

12

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
)
Neal Ehardt
  • 10,334
  • 9
  • 41
  • 51
9

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.

Yuji
  • 34,103
  • 3
  • 70
  • 88
  • Thanks for introducing me to Objective-C blocks. Never saw this caret thing until now... When I try to use your code (as added as edit to the question), I get the following compiler error: `incompatible block pointer types initializing 'struct NSString * (^)(struct objc_object *)', expected 'struct objc_object * (^)(struct objc_object *)'`. I haven't mastered blocks enough yet to know what is wrong. – pesche Jan 02 '11 at 21:49
  • The [Apple documentation on Blocks](http://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/Blocks/Articles/00_Introduction.html) states that Blocks can be used on iOS 4 and later. What happens when I try to run the app with iOS 3.1.x? – pesche Jan 02 '11 at 21:55
  • Thanks again! Figured where I had to change the return type and it works perfectly now! And I will have to think if I want to support iOS; nice to know the links where I'd have to start! – pesche Jan 03 '11 at 06:09
  • In fact it's easy to support your suggestion `[array transform:stringBy...]`. See above. – Yuji Jan 03 '11 at 06:47