Добавляем Pattern Matching и параметризованные методы в Objective-C

Все больше и больше статей на тему «добавь функциональные косты плюшки в свой любимый императивный язык программирования». Вот недавний пример для Java.

В Objective-C не так давно были добавлены блоки (blocks), с помощью которых реализованы замыкания. Но хочется чего-то большего. Например сопоставления с образом (Pattern Matching) и параметризованные методы.

Исключительно Just For Fun попробуем добавить их в язык без патчинга компилятора и танцев с препроцессором, только средствами самого языка.

Что из этого получилось?

Все описанное далее исключительно пример иллюстрирующий идею и использование его в своих проектах вы можете на свой страх и риск.

Cопоставление с образом позволяет написать что-то на подобии:
factorial( 0 ) -> 1
factorial( 1 ) -> 1
factorial( n ) -> n * factorial( n - 1 )


Для начала, пробуем сделать сопоставление с образом. Итак, создаем расширение NSObject и объявляем тип блока, в который мы и будем заворачивать дополнительные реализации.
typedef id (^PatternMatchingBlock)(id obj);
 
@interface NSObject (PatternMatching)
-(void)method:(SEL)sel_ withParameter:(id)object_ useBlock:(PatternMatchingBlock)block_;
@end

Теперь нужно подменить реализацию селектора чтобы иметь возможность выбирать необходимую реализацию в зависимости от переданного фактического параметра. В этом поможет прием подмены метода (Method Swizzling). Каждый метод — это структура типа objc_method из которой нас интересует поле method_imp типа IMP. IMP — это указатель на C-функцию реализации метода. Смысл подмены метода — в замене этих указателей у 2-х методов. Создаем класс, который будет хранить в себе указатель на изначальную реализацию метода и словарь, ключами которого будут объекты «образов», а значениями — блоки реализаций:
@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

И собственно реализация расширения NSObject:
@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

Вся магия — в методе [-NSObject method:withParameter:useBlock:]. Для переданного селектора мы создаем объект PMImplementation, который сохраняет в себе указатель на реализацию. После чего заменяем его на метод «swizzledMethod:». Трюк в том, что мы можем узнать какой селектор был вызван на самом деле через параметр _cmd, который неявно передается при вызове любого селектора. Теперь при вызове swizzledMethod вызывается [-PMImplementation forObject:invokeWithParameter:] а там мы уже либо находим по объекту блок и выполняем его, либо используем реализацию по умолчанию.
Метод [-NSObject impls] добавляет в рантаме для self словарь, хранящий объекты PMImplementation для разных селекторов.

Теперь то, ради всего все это затевалось — например класс для нахождения факториала:
@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]]]];
}


Работает он как и предполагалось:
   Factorial* factorial_ = [[Factorial new] autorelease];
   NSNumber* number_ = [NSDecimalNumber numberWithInteger:10];
   NSLog( @"factorial %@ = %@", number_, [factorial_ factorial:number_]);

factorial 10 = 3628800

Для параметризации методов напишем небольшой класс-обертку, у которого переопределим метод «isEqual:» и удобный макрос.
#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
 

Теперь реализацию можно выбирать и в зависимости от типа аргумента.
@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

Пробуем:
   Test* test_ = [[Test new] autorealese];
   [test_ test:@"String"];
   [test_ test:[NSNull null]];

default impl for Object: NSCFString
implementation for Null: NSNull


Что можно было сделать еще? Как минимум работу с методами у которых больше одного аргумента, но это сильно бы увеличило статью.
В итоге получены реализации сопоставления с образом и параметризованных методов (правда с жутким синтаксисом) в Objective-C.

Очевидные минусы — в качестве параметров и «образов» можно использовать только объекты.

Надеюсь эта статья была хоть кому-то полезна и после прочтения возникло желание узнать побольше о магии рантайма Objective-C. Если да, то увлекательное чтиво на ночь:
Objective-C Runtime Programming Guide
Objective-C Runtime Reference
  • +12
  • 1,9k
  • 8
Поделиться публикацией

Похожие публикации

AdBlock похитил этот баннер, но баннеры не зубы — отрастут

Подробнее
Реклама

Комментарии 8

    0
    Что магия то магия…
      +1
      Почему-то вспомнился сборник «Физики шутят» :)
        +2
        Почему какждый раз, как я вижу синтаксис Objective-C, у меня мозг сворачивается в трубочку? :)

        А так — даа, магия так магия
          +6
          Я, пока не начал на нём писать испытывал что-то подобное. Стоит пописать месяц, и он кажется в 10 раз читаемее, чем любой другой язык. За счёт именованных параметров, в основном. Хотя его динамичность и фактическое отсутствие типизации в рантайме мне не нравится.
            0
            Аналогично. После нескольких недель [object method:1] читается одинаково с object->method(1) или object.method(1). А динамичность языка позволяет писать более простой и понятный код.
            0
            аналогично… вынос мозга!
            0
            А можно небольшой оффтоп по коду для тех, «кто в танке» — почему после переменных "_"?? У меня глаз с непривычки еле жив.
              0
              "_" после переменных означает что переменная локальная. Сделано для того чтобы отличать их от свойств и ivar'ов когда читаешь код.

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое