Rx with Objective-C using NSInvocation
I want to make a point that NSInvocation
is a really powerful tool. If we do not care too much about a compiler type checking every single line of our code we can build a scalable Rx implementation using NSInvocation
.
NSInvocation
is an Objective-C message rendered as an object that can be passed around and invoked when required. Think of it as an old school closure which isn’t anonymous, rather opaque in nature and doesn’t implicitly captures any data, close to C++ lambda expressions. NSInvocation
is just a simple wrapper around a function, function object would be the best term to describe it.
NSInvocation
is very verbose. Here’s an example of an NSInvocation
that captures the print:
method:
NSInvocation *verbose = [NSInvocation invocationWithMethodSignature:
[self methodSignatureForSelector:@selector(print:)]];
[verbose setTarget:self];
[verbose setSelector:@selector(print:)];
Since we are going to use NSInvocation
quite a lot, it would be wise if we write a nice category to reduce verbosity a bit.
@implementation NSInvocation (RxConvenience)
+ (instancetype)invocationWithTarget:(id)target
selector:(SEL)selector
{
NSInvocation *invoc = [NSInvocation invocationWithMethodSignature:
[target methodSignatureForSelector:selector]];
[invoc setTarget:target];
[invoc setSelector:selector];
return invoc;
}
@end
With this change the same expression as above can now be written as:
NSInvocation *lessVerbose = [NSInvocation invocationWithTarget:self
selector:@selector(print:)];
Rx
At the core of Rx is the idea of producers, consumers and events. A producer generates some events and a consumer listens to those events. Sometimes the data expected by the consumer is not exactly what the producer is emitting, in that case we need an operator that knows how to convert data from a form to another. Other times, a consumer needs to listen to events from more than one consumers, for that again we have an operator that can convert multiple data into a single data. As you can imagine these single purpose operators can be very powerful in building giant sophisticated systems for delivering events from producer to consumer. And this plumbing of many small components together to perform a bigger task is what makes Rx so great!
Observables
So, before we go on writing any code lets imagine how do wish our Rx API to look like from the client perspective. If we have a simple print:
method as
- (void)print:(NSNumber *)num
{
NSLog(@"%@", num);
}
We would like Rx to expose a way to create a producer and a way to consume the events, something like:
- (void)run
{
RxObservable *stream = [RxObservable observableFromArray:@[@1, @2, @3]];
NSInvocation *printInvoc = [NSInvocation invocationWithTarget:self
selector:@selector(print:)];
[stream subscribe:printInvoc];
}
As a initial step let’s define a base class called RxObservable
that provides this interface. And since we don’t want to bloat this class with every spec implementation, let’s provide some factory methods to some internal implementation specific class.
@implementation RxObservable
+ (instancetype)observableFromArray:(NSArray *)array;
{
return [RxArraySource createWithElements:array];
}
- (void)subscribe:(NSInvocation *)invocation;
{
// Needs to be subclassed
}
@end
And then the actual implementation could look something like
@interface RxArraySource : RxObservable
+ (instancetype)createWithElements:(NSArray *)array;
- (void)subscribe:(NSInvocation *)invocation;
@end
@interface RxArraySource ()
@property (nonatomic, copy) NSArray *elements;
@end
@implementation RxArraySource
+ (instancetype)createWithElements:(NSArray *)elements;
{
RxArraySource *instance = [RxArraySource new];
instance.elements = elements;
return instance;
}
- (void)subscribe:(NSInvocation *)invocation;
{
for (id element in _elements) {
[invocation setArgument:&element atIndex:2];
[invocation invoke];
}
}
@end
With that in place, our run
method should be working as expected.
1
2
3
Operators
Next lets see how can we go about writing an Rx operator. One of the most commonly used Rx operator is the map
operator which simply converts data from one type to another. Again, thinking upside down, how would we want our API to look like in the end.
If we have a standalone method like
- (NSNumber *)tenTimesOfNumber:(NSNumber *)num
{
return [NSNumber numberWithInteger:[num integerValue] * 10];
}
We should be able to create a map operator which can then be plugged in with rest of the pipeline
- (void)run
{
RxObservable *stream = [RxObservable observableFromArray:@[@1, @2, @3]];
NSInvocation *printInvoc = [NSInvocation invocationWithTarget:self
selector:@selector(print:)];
NSInvocation *mapInvoc = [NSInvocation invocationWithTarget:self
selector:@selector(tenTimesOfNumber:)];
[[stream mapWithInvocation:mapInvoc] subscribe:printInvoc];
}
Lets extend our RxObservable
to provide an interface for this, which again would be more like a factory method to an implementation specific class.
@implementation RxObservable
// ...
- (instancetype)mapWithInvocation:(NSInvocation *)invo
{
return [RxMapOperator operatorFromStream:self withInvocation:invo];
}
@end
Notice since we are always passing RxObservable
from our fat base class, we can easily chain RxObservables
together. Now let’s go and write RxMapOperator
@interface RxMapOperator : RxObservable
+ (instancetype) operatorFromStream:(RxObservable *)stream
withInvocation:(NSInvocation *)invoc;
@end
The thing to realize with a map
operator is that it knows not a lot about the actual types. It is provided with an actual implementation from the client that knows how to map types. So RxMapOperator
only needs to chain two methods, take returned value from one and pass it down to the another.
@interface RxMapOperator ()
@property (nonatomic, strong) RxObservable *stream;
@property (nonatomic, strong) NSInvocation *invocUp;
@property (nonatomic, strong) NSInvocation *invocDown;
@end
@implementation RxMapOperator
+ (instancetype) operatorFromStream:(RxObservable *)stream
withInvocation:(NSInvocation *)invoc;
{
RxMapOperator *instance = [RxMapOperator new];
instance.stream = stream;
instance.invocUp = invoc;
return instance;
}
- (void)transform:(id)arg
{
[self.invocUp setArgument:&arg atIndex:2];
[self.invocUp invoke];
id ret;
[self.invocUp getReturnValue:&ret];
[self.invocDown setArgument:&ret atIndex:2];
[self.invocDown invoke];
}
- (void)subscribe:(NSInvocation *)invocation
{
self.invocDown = invocation;
[self.stream subscribe:[NSInvocation invocationWithTarget:self
selector:@selector(transform:)]];
}
@end
With that in place our run
method should now print
10
20
30
Parting thoughts
This experiment was just to play around the idea of building something quick and dirty with NSInvocation
. Obviously none of this code is production ready, but I think it can be expanded to cover all sort of building blocks for Rx patterns.
Here is the source code, feel free to checkout and play with it gist.github.com/chunkyguy/0f67f074c974aac7024d153011e5400e