📜 ⬆️ ⬇️

Add Pattern Matching and Parameterized Methods in Objective-C

More and more articles on the topic “add functional buns to your favorite imperative programming language”. Here is a recent Java example .

In Objective-C, blocks (blocks) were added not so long ago, by means of which closures are implemented. But I want something more. For example, matching with the image ( Pattern Matching ) and parameterized methods.

Exclusively Just For Fun try to add them to the language without patching the compiler and dancing with the preprocessor, using only the means of the language itself.
')
What came of it?

Everything described below is an example illustrating the idea and using it in your projects at your own risk.

Matching the image allows you to write something like:
factorial( 0 ) -> 1
factorial( 1 ) -> 1
factorial( n ) -> n * factorial( n - 1 )


To begin with, we try to make a comparison with the image. So, we create the NSObject extension and declare the type of the block into which we will wrap additional implementations.
typedef id ( ^ PatternMatchingBlock ) ( id obj ) ;

@interface NSObject ( PatternMatching )
- ( void ) method : ( SEL ) sel_ withParameter : ( id ) object_ useBlock : ( PatternMatchingBlock ) block_;
@end

Now you need to replace the implementation of the selector to be able to choose the necessary implementation, depending on the actual parameter passed. This will help the reception method substitution ( Method Swizzling ). Each method is a structure of type objc_method from which we are interested in the field method_imp type IMP . IMP is a pointer to the C function of the method implementation. The meaning of the method substitution is in replacing these pointers in 2 methods. Create a class that will hold a pointer to the original implementation of the method and a dictionary, the keys of which are the objects of “images”, and the values ​​are the implementation blocks:
@interface PMImplementation : NSObject
@property ( nonatomic, retain ) NSMutableDictionary * impls;
@property ( nonatomic, retain ) NSValue * defaultImpl;
+ ( id ) implementationWithDefaultImpl : ( IMP ) impl_;
- ( id ) forObject : ( id ) object_ invokeWithParameter : ( id ) parameter_;
@end

static char * PMimplsKey = nil ;

@implementation PMImplementation
@synthesize impls = _impls;
@synthesize defaultImpl = _default_impl;
- ( void ) dealloc {
[ _impls release ] ;
[ _default_impl release ] ;
[ super dealloc ] ;
}
- ( id ) initWithDefaultImpl : ( IMP ) impl_ {
if ( ! ( self = [ super init ] ) )
return nil ;
self.defaultImpl = [ NSValue valueWithPointer : impl_ ] ;
self.impls = [ NSMutableDictionary dictionary ] ;
return self;
}
+ ( id ) implementationWithDefaultImpl : ( IMP ) impl_ {
return [ [ [ self alloc ] initWithDefaultImpl : impl_ ] autorelease ] ;
}
- ( id ) forObject : ( id ) object_ invokeWithParameter : ( id ) parameter_ {
for ( id key_ in [ self.impls allKeys ] )
if ( [ key_ isEqual : parameter_ ] ) {
PatternMatchingBlock block_ = [ self.impls objectForKey : key_ ] ;
return block_ ( parameter_ ) ;
}
IMP impl_ = [ self.defaultImpl pointerValue ] ;
return impl_ ( object_, _cmd, parameter_ ) ;
}
@end

And the actual implementation of the NSObject extension:
@implementation NSObject ( PatternMatching )
- ( NSMutableDictionary * ) impls {
NSMutableDictionary * impls_ = objc_getAssociatedObject ( self, & PMImplsKey ) ;
if ( ! impls_ ) {
impls_ = [ NSMutableDictionary dictionary ] ;
objc_setAssociatedObject ( self, & PMImplsKey, impls_
, OBJC_ASSOCIATION_RETAIN_NONATOMIC ) ;
}
return impls_;
}
- ( void ) method : ( SEL ) sel_ withParameter : ( id ) object_
useBlock : ( PatternMatchingBlock ) block_ {
NSString * selector_key_ = NSStringFromSelector ( sel_ ) ;
PMImplementation * impl_ = [ self.impls objectForKey : selector_key_ ] ;
if ( ! impl_ ) {
Method default_ = class_getInstanceMethod ( [ self class ] , sel_ ) ;
IMP default_impl_ = method_getImplementation ( default_ ) ;
impl_ = [ PMImplementation implementationWithDefaultImpl : default_impl_ ] ;
[ self.impls setObject : impl_ forKey : selector_key_ ] ;
Method swizzed_method_ = class_getInstanceMethod ( [ self class ]
, @selector ( swizzledMethod :) ; ) ;
method_setImplementation ( default_, method_getImplementation ( swizzed_method_ ) ) ;
}
[ impl_.impls setObject : block_ forKey : object_ ] ;
}

- ( id ) swizzledMethod : ( id ) obj_ {
PMImplementation * impl_ = [ self.impls objectForKey : NSStringFromSelector ( _cmd ) ] ;
return [ impl_ forObject : self invokeWithParameter : obj_ ] ;
}
@end

All magic is in the [-NSObject method: withParameter: useBlock:] method. For the passed selector, we create a PMImplementation object that stores the pointer to the implementation. Then we replace it with the “swizzledMethod:” method. The trick is that we can find out which selector was actually called via the _cmd parameter, which is implicitly passed when any selector is called. Now when calling swizzledMethod, [-PMImplementation forObject: invokeWithParameter:] is called, and then we either find a block on the object and execute it, or use the default implementation.
The [-NSObject impls] method adds a self-dictionary in the runtam that stores PMImplementation objects for different selectors.

Now, for the sake of all this, it was started - for example, a class for finding factorial
@interface Factorial : NSObject
- ( NSDecimalNumber * ) factorial : ( NSDecimalNumber * ) number_;
@end

@implementation Factorial
- ( id ) init {
if ( ! ( self = [ super init ] ) )
return nil ;

NSDecimalNumber * zero_ = [ NSDecimalNumber numberWithInteger : 0 ] ;
[ self method : @selector ( factorial :) withParameter : zero_ useBlock : ^ ( id obj_ ) {
return ( id ) [ NSDecimalNumber numberWithInteger : 1 ] ;
} ] ;
NSDecimalNumber * one_ = [ NSDecimalNumber numberWithInteger : 1 ] ;
[ self method : @selector ( factorial :) withParameter : one_ useBlock : ^ ( id obj_ ) {
return ( id ) [ NSDecimalNumber numberWithInteger : 1 ] ;
} ] ;

return self;
}
- ( NSDecimalNumber * ) factorial : ( NSDecimalNumber * ) number_ {
return [ number_ decimalNumberByMultiplyingBy :
[ self factorial : [ number_ decimalNumberBySubtracting :
[ NSDecimalNumber numberWithInteger : 1 ] ] ] ] ;
}


It works as expected:
Factorial * factorial_ = [ [ Factorial new ] autorelease ] ;
NSNumber * number_ = [ NSDecimalNumber numberWithInteger : 10 ] ;
NSLog ( @ "factorial% @ =% @" , number_, [ factorial_ factorial : number_ ] ) ;

factorial 10 = 3628800

To parameterize the methods, we write a small wrapper class in which we override the “isEqual:” method and a convenient macro.
#define PMCLASS (x) [[[PMClass alloc] initWith: [x class]] autorealese]
@interface PMClass : NSObject <NSCopying>
@property ( nonatomic, retain ) Class class;
- ( id ) initWith : ( Class ) class_;
@end

@implementation PMClass
@synthesize class = _class;
- ( void ) dealloc {
[ _class release ] ;
[ super dealloc ] ;
}
- ( id ) initWith : ( Class ) class_ {
if ( ! ( self = [ super init ] ) )
return nil ;
self.class = class_;
return self;
}
- ( BOOL ) isEqual : ( id ) object_ {
return [ self.class isEqual : [ object_ class ] ] ;
}
- ( id ) copyWithZone : ( NSZone * ) zone_ {
return [ [ PMClass alloc ] initWith : self.class ] ;
}
@end

Now the implementation can be chosen depending on the type of the argument.
@interface Test : NSObject
- ( void ) test : ( id ) obj_;
@end

@implementation Test
- ( id ) init {
if ( ! ( self = [ super init ] ) )
return nil ;
[ self method : @selector ( test :) withParameter : PMCLASS ( NSNull ) useBlock : ^ ( id obj_ ) {
NSLog ( @ "implementation for Null:% @" , [ obj_ class ] ) ;
return ( id ) nil ;
} ] ;
return self;
}
- ( void ) test : ( id ) obj_ {
NSLog ( @ "default impl for Object:% @" , [ obj_ class ] ) ;
}
@end

We try:
Test * test_ = [ [ Test new ] autorealese ] ;
[ test_ test : @ "String" ] ;
[ test_ test : [ NSNull null ] ] ;

default impl for Object: NSCFString
implementation for Null: NSNull


What else could be done? At a minimum, working with methods that have more than one argument, but this would greatly increase the article.
As a result, we obtained implementations of comparison with the image and parameterized methods (albeit with a creepy syntax) in Objective-C.

Obvious disadvantages - only objects can be used as parameters and “images”.

I hope this article was at least helpful to someone, and after reading, there was a desire to learn more about Objective-C runtime magic. If so, then an exciting reading for the night:
Objective-C Runtime Programming Guide
Objective-C Runtime Reference

Source: https://habr.com/ru/post/123187/


All Articles