October 8, 2005

Pimp My Code, Interlude: Free Code

Ok, I wanted to share a file from my shared source directory, because I don't have very many things in my shared source directory. I'm very picky this time about what I consider "shared" -- I have to actually USE code in two different projects to consider it shared, not just think "Hmm, someday somebody may want to re-use this." Because, in truth, most of the crap people put into shared code directories is either too specific to really be shared, OR (more commonly) it's written in a general way but the particular app it was written for only tested some of the pathways, so it is essentially a bunch of buggy code that's not actually being used and is waiting to trip you up.

And, yes, if you'd like you can compound this mess by writing test cases for the code you don't use, to prove that it does what you think it might do even though nobody actually cares. And, yes, I've been paid to do this for people.

All that said, here's the single-most used file in my shared repository:

DMCommonMacros.h
static inline BOOL IsEmpty(id thing) {
return thing == nil
|| ([thing respondsToSelector:@selector(length)]
&& [(NSData *)thing length] == 0)
|| ([thing respondsToSelector:@selector(count)]
&& [(NSArray *)thing count] == 0);
}


Yup, that's the whole file. You WOULD NOT BELIEVE how often I use this macro. At least once per method, often twice or three times.

Essentially, if you're wondering if an NSString or NSData or NSAttributedString or NSArray or NSSet has actual useful data in it, this is your macro. Instead of checking things like "if (inputString == nil || [inputString length] == 0)" you just say, "if (IsEmpty(inputString))".

Seriously, this code may seem butt-obvious but you're going to find a million places in your code to use it. Pretty much every time I was just checking for nil, I now call IsEmpty(), because, hey, if someone just passed me in an empty stub of a string or array or whatever, that's still empty. I probably want to do the same thing.

This is especially important in Cocoa, because it's Apple's convention to return empty objects instead of 'nil' if the set of things you are looking for is empty; for instance, if you call -selectedObjects on a Cocoa instance that has no selection, you can expect to get an empty NSArray, not nil. (A glaring exception to this is the IOBluetooth toolkit, which sometimes returns nil instead of an empty array when you ask for a list of devices, which can break your code silently and badly if you're not expecting it. I filed a bug on this (RADAR #4093573) but, sadly, the engineers decided their behavior was correct, even though it Cocoa's stated conventions.)

Apple has also (mostly) cracked down on passing in 'nil' to Cocoa when you should pass in some empty object -- most of the time you'll get an exception thrown nowadays if you, say, set something to have a string value of 'nil' instead of @"" (c.f. NSMutableString documentation).

However, I use 'nil' all the time in my code, so at any given time, for any given method, I'm never really sure if empty input will be some empty object or 'nil'. Hence, IsEmpty(), which lets my code function perfectly without worrying what convention the caller is using.

Labels:

21 Comments:

Anonymous Darth Sidious said...

Maybe I'm a premature optimization myself but are you not killing performance by using this "MACRO"?

Because :

- There is a long path that could be shortened if you were dealing with more specific MACROs. Your code is good but it's a bit like determining if an animal is a dog by enumerating all the species in the world whereas you already know it has four legs, a tail and barks.

- You're sending messages when this could be avoided.

October 08, 2005 4:38 AM

 
Blogger Wil Shipley said...

Yes, you are pre-optimizing. On average this will add a method call to your comparisons, because you check to see if the object responds to -length before sending it -length.

I could use a different macro for each type of thing in the world, but I decided this made the code a ton cleaner. Less code is better code. Less interfaces that do the exact same thing is better.

Seriously, it's an object-oriented language. If you're counting method calls as you write, you're going to make yourself nuts. There are a LOT of method calls in Objective-C. Millions and millions happen before your app even calls -applicationDidFinishLaunching:.

Yes, if you're in a tight loop, and you're looping over thousands of items, then sure, cut down on the method calls. Optimize THEN.

But, seriously, if you're writing some generic method, the most important thing is to make your code bullet-proof and clear. IsEmpty(blah) is really quite unambiguous, and I find it very easy to read. Especially because it is very often my bail code, so I'm all "if (IsEmpty(foo)) break..." and that's that. The consistency of it makes it easier to read.

Think about how many thousands of times a routine will be called per minute. Most of the code we write isn't called that often. Your processor can handle at least a BILLION operations EVERY SECOND. A method call, in the optimized case, takes 12 of those BILLION. You have to be calling a LOT of methods in order to make this slow.

-W

October 08, 2005 4:48 AM

 
Anonymous Jesper said...

Now, I'm not even sure this fits the criteria for a specific work, but since you're posting it here and calling it free code, are you in fact giving it away in the public domain or in some kind of inferred BSD license?

October 08, 2005 5:07 AM

 
Blogger Wil Shipley said...

I'm publishing it under the DoNotSueMe license.

Also, I think you mean implied, not inferred.

-W

October 08, 2005 5:15 AM

 
Anonymous Jesper said...

Yes. Implied. And I guess it's also implied that one can include "Wil Shipley contributed one macro to this code by putting it on his web log, and he's an overall nice and decent guy with exquisite taste in the mindboggingly vast field of shirts" if one wants to, but seeming as how it's "free" you don't need to.

October 08, 2005 5:28 AM

 
Anonymous Brian Webster said...

Wil,

I'm curious as to your opinion on sending messages to nil objects. Since a message to nil will return nil or 0 (with the exception of structs, doubles, etc.), if you have an array object, for instance, you could check to see if it's empty just by testing if ([myArray count] == 0). Both an empty array and a nil array will return 0 in this case. Do you ever do this sort of thing, and if not, why not?

October 08, 2005 7:14 AM

 
Anonymous Waterspirit said...

Darth Sidious and Wil, supposing how about using a C++ template with specializations?

Unless you're dependent on Objective C99, that would be a clean solution. And doing that, there's no need for lengthy arguments about the nature of object orientation only to justify an unnecessary horizontal inefficiency.

October 08, 2005 7:16 AM

 
Anonymous Greg Titus said...

Brian: I always let methods to nil give me the result I want when it is appropriate, but it wouldn't help for this method of Wil's.

You need to explicitly check for nil, or else both of the -respondsToSelector: calls will be NO, and calling isEmpty() on nil will return NO.

October 08, 2005 8:50 AM

 
Anonymous Greg Titus said...

By the way Wil, if you ever need to deal with potentially weird unicode strings then it is possible to have an NSString where [string length] > 0, and yet, [string isEqualToString:@""] is true. (I.e. it is equal to an empty string but it does have characters in it.) In many cases these strings should probably be treated the same way as a zero length string.

So you might want to have a:
static inline BOOL isEmptyString(NSString *string) {
return string == nil || [string isEqualToString:@""];
}

Leave it to Unicode to complicate things.

October 08, 2005 8:58 AM

 
Anonymous cesar said...

hey, wil, did you ever thing in having a position as Sr. Architect or evangelist in Apple?

from your postings, I can see that you LOVE coding!

October 08, 2005 4:09 PM

 
Blogger Wil Shipley said...

Working at Apple used to be my dream. And actually, Steve did eventually (casually) ask me to work for him a couple times, back in the old days. Oddly, when Apple hired away the rest of my company recently they didn't approach me again.

Maybe they knew someone had to write Delicious Library 2.

October 08, 2005 4:20 PM

 
Anonymous Anonymous said...

Hmm... Looks like a good idea for a high-order message, too.

-jcr

October 08, 2005 11:01 PM

 
Anonymous Anonymous said...

Hmm. I'm not seeing the usefulness of this one. Since Obj-C guarantees a nil object to return nil to all messages, and since I nearly always know the type of an object whose emptiness I'm checking,

if ([array count])

or

if ([string length])

is sufficient. I would never, ever, ever write

"if (inputString == nil || [inputString length] == 0)"

October 09, 2005 8:10 AM

 
Anonymous Anonymous said...

I would never, ever, ever write
"if (inputString == nil || [inputString length] == 0)"


I think, Wil wants to use isEmpty for ANY kind of object, and hence needs to check that the "thing" responds to a selector before using it, so the easiest way is to test for nil first and bail if necessary, so using the 'respondsToSelector' method makes sense.

October 09, 2005 8:27 AM

 
Anonymous Karl Adam said...

Actually, that whole tracking down any use of nil thing isn't true. CoreData for example allows you to create an NSManagedObject outside of an NSManagedObjectContext if you call NSManagedObjects' initWithEntity:inContext: with nil as the context.

So Apple isn't so much removing them as just not documenting useful functionality. Then again this could be a bug and I should wave my hands here so you all forgot and don't go report this wonderfully useful functionality.

October 10, 2005 6:59 AM

 
Anonymous Marcel Weiher said...

Of course, the easy way to solve this and take advantage of nil-messaging is to have a message called -(BOOL)isNotEmpty;

Then add this as a category wherever you need it. You should never use explicit type/respondsTo tests where you can use polymorphism to achieve the same effect. For example, this also keeps weird Unicode string handling in you Unicode string class, instead of polluting global header files!

This also keeps your tests positive, such as the following:

if ( [stuff isNotEmpty] ) {
...do stuff with stuff...
}

vs.

if ( isEmpty( stuff ) ) {
..bail..
} else {
..do stuff with stuff..
}

or

if ( !isEmpty( stuff ) ) {
..do stuff with stuff...
}

As John points out, you could probably also create a HOM for this...

October 10, 2005 5:27 PM

 
Anonymous cesar said...

wil,

I think you make a great move by not going to apple. you have your company and you handle your company as you wish and you just enjoy working there. I wish I could go and create my company, to work, learn and have fun!

but I also think that you could be a GREAT cocoa evangelist @ apple

October 10, 2005 8:26 PM

 
Anonymous Anonymous said...

This is a silly comment, but it's bothering me: the bit of code in question is not a macro, but rather an inline function.

October 20, 2005 5:03 AM

 
Blogger jm3 said...

i have an identical method except mine's named isUseless(). it gets used constantly.

April 30, 2006 3:43 PM

 
Anonymous Peter Jaros said...

Marcel wrote:

This also keeps your tests positive, such as the following:

if ( [stuff isNotEmpty] ) {
...do stuff with stuff...
}

vs.

if ( isEmpty( stuff ) ) {
..bail..
} else {
..do stuff with stuff..
}


Fair point, but that's not Wil's style. If I may put words in his mouth, Wil prefers to bail--really bail--and then continue processing. That means Wil's version becomes:

if ( isEmpty( stuff ) ) {
..bail..
}

..do stuff with stuff..

That separates the bail code and puts the main code at the first level of the method, indentation- and block-wise.

May 18, 2006 3:07 PM

 
Anonymous Anonymous said...

i'm not really qualified to post anything here- just wanted to say i like the use of 'short-circuit' logic in Wil's original post... checking for nil before sending messages, checking 'respondsTo' before sending further messages, etc. knowing all the while how the compiler works, and why that sequence is the right way to do it.

that sort of safe coding is valuable to newbies like me, even when we can't offer any valuable arguments for/against the finer points.

thanx, Wil.

September 15, 2006 9:02 PM

 

Post a Comment

<< Home