I’m reading Kent Beck’s Smalltalk Best Practice Patterns and learning a lot about how control structures work in what’s probably the purest object-oriented language. For example, if
isn’t used to manage conditionals. In Smalltalk, since all things are objects, including booleans, you can just send the message -ifTrue:
and -ifFalse:
, with blocks, to the True object.
Let’s try that out with Objective-C:
@implementation NSNumber (IfTrue)
- (BOOL)isBoolean {
return [@([self objCType]) isEqualToString:@(@encode(BOOL))];
}
- (id)ifTrue:(void (^)())onTrue {
if ([self isBoolean]) {
if ([self boolValue]) {
if (onTrue) onTrue();
} else {
return self;
}
}
return nil;
}
- (id)ifFalse:(void (^)())onFalse {
if ([self isBoolean]) {
if ([self boolValue]) {
return self;
} else {
if (onFalse) onFalse();
}
}
return nil;
}
@end
When the case is not satisfied (i.e., the NSNumber
is @YES
, but we messaged -ifFalse:
), we return self
, so that the messages can be chained, like so:
[[@YES ifTrue:^{
NSLog(@"this code block is entered!");
}] ifFalse:^{
NSLog(@"this one is not");
}];
Pretty elegant! Smalltalk also allows us to ask an object to execute block if it’s nil. Hm. This actually presents a problem for us, since nil
in Objective-C eats all messages and returns nil
again. Our message would be swallowed and our block wouldn’t get executed. Fortunately, the runtime is here to save us! Let’s #import <objc/runtime.h>
. Grab the whiskey.
Bill Bumgarner teaches us how to create our own nil object and tell the runtime that we’d like to use it instead of the built in nil
. First, let’s make STNil
. It eats all messages, just like good old regular nil
:
@implementation STNil
+ (BOOL)resolveClassMethod:(SEL)sel { return NO; }
+ (BOOL)resolveInstanceMethod:(SEL)sel { return NO; }
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [NSObject instanceMethodSignatureForSelector: @selector(description)];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation { }
- (void)ifNil:(void (^)())onNil {
if (onNil) onNil();
}
@end
And let’s add the -ifNil:
message to NSObject
as a category and make it a no-op:
@implementation NSObject (IfNil)
- (void)ifNil:(void (^)())onNil { }
@end
And then lets use _objc_setNilReceiver
(which is private!) to set STNil
as the nil receiver;
extern id _objc_setNilReceiver(id newNilReceiver);
int main(int argc, const char **argv) {
@autoreleasepool {
id smalltalkyNil = [STNil new];
_objc_setNilReceiver(smalltalkyNil);
NSString *nilString = nil;
[nilString ifNil:^{
NSLog(@"this object is nil");
}];
_objc_setNilReceiver(nil);
}
return 0;
}
This works! We can now message nil as easily as any other object.
Let’s take it even further. In Ruby on Rails, objects support something called presence
. Presence makes truthiness behave in a more useful and consistent way. Nil, empty strings, and empty collections are “not present” (or “blank”), so you can call myArray.blank
instead of the more convoluted myArray.count == 0
. Let’s bring that stuff over to this new block based model as well.
Let’s put some more stuff on NSObject
:
@implementation NSObject (Presence)
- (BOOL)present {
return YES;
}
- (BOOL)blank {
return !self.present;
}
- (void)ifPresent:(void (^)())onPresent {
if (self.present && onPresent) onPresent();
}
- (void)ifBlank:(void (^)())onBlank {
if (self.blank && onBlank) onBlank();
}
@end
And then, all we have to do create a category on NSArray
that overrides present
(and optionally forward -ifEmpty:
to -ifBlank:
):
@implementation NSArray (Presence)
- (BOOL)present {
return !!self.count;
}
- (void)ifEmpty:(void (^)())onEmpty {
[self ifBlank:onEmpty];
}
@end
Now, we can call -ifEmpty:
exactly as we’d expect to:
[@[] ifEmpty:^{
NSLog(@"this array is empty!");
}];
Who knew Smalltalk could be so much fun?