Пару дней назад решил поизучать лиспы (наконец-то). Естественно, первая же мысль — а можно ли их связать с Cocoa? Есть Clozure CL — Common Lisp для Мака, и я его еще не смотрел, но мне захотелось чего-нибудь очень маленькое, что можно встроить в свои приложения. И вспомнил про TinyScheme — имплементацию Scheme в ~6000 строках кода. (Кстати, Apple с 10.5 использует TinyScheme для задания правил сэндбоксинга программ.)
Исходники скаченные с сайта не компилировались, но там была инструкция как сделать так, чтобы все заработало. Впрочем, она там какая-то странная (а может не обновлялась давно) — я поменял меньше, чем там написано и получилось. Естественно, сразу выложил работающую версию на GitHub, заодно изучил, как с ним общаться из Си, написал example.c, ну и потом понеслось — захотелось общаться с Objective-C из Scheme. Одновременно учил Scheme и писал код. Получилось интересно.
Интерфейс с Scheme у нас будет классом TinyScheme.
TinyScheme *ts = [[TinyScheme alloc] init];
Та-дам! Интерпретатор готов. (Можно создавать сколько угодно объектов — у каждого будет свой интерпретатор.)
Запустим какую-нибудь ерунду:
[ts loadString:@"(log \"Hello, world!\")"];
Что выполнит (log "Hello, world!").
Или файл:
[ts loadFileWithPath:@"init.scm"];
Что интерпретирует файл init.scm.
Если интерпретатор выдаст ошибку, ts выкинет исключение, которое можно поймать в @try...@catch:
@try {
[ts loadString:@"((we-have error here)"];
}
@catch (NSException *e) {
NSLog(@"The exception was: %@ reason: ``%@'')", [e name], [e reason]);
}
Больше примеров тут.
Ну а теперь самое интересное: работа с Objective-C из Scheme. Если мы хотим, чтобы объект был доступен в Scheme, нужно его зарегистрировать:
NSNumber *magicNumber = [NSNumber numberWithInt:42];
[ts registerObject:magicNumber withName:@"magicNumber"];
Он будет известен Scheme под символом magicnumber (заметьте, что он оказался в нижнем регистре, потому что Scheme нечувствителен к регистру).
Теперь мы можем посылать ему сообщения, например:
(obj-send 'magicNumber "className")
вернет строку с именем класса (“NSCFNumber”), то есть сделает то же самое, что [magicNumber className] в Objective-C.
Можно и сократить:
(-> 'magicNumber "className")
потому что -> определен в objc.scm как:
(define -> objc-send)
Там еще несколько хэлперов есть.
Заметьте, что класс TinyScheme автоматически конвертирует Scheme типы в C/ObjC и обратно. Здесь мы вызвали метод className, который возвращает NSString *, а внутри Scheme он уже оказывается Scheme-типом string. Или, например, если у нас есть объект test класса Test в Objective-C с таким методом:
- (int)doSomethingWithDouble:(double)d andString:(NSString *)s
{
NSLog(@"string was: %@", s);
return 2 * d;
}
то мы можем его вызывать из Scheme вот так:
(-> 'test "doSomethingWithDouble:andString:" 7.40 "Hello")
и получим результат 14 (метод возвращает int), с которым можем обычным образом работать:
(+ 10 (-> 'test "doSomethingWithDouble:andString:" 7.40 "Hello"))
вернет 24.
Оки-доки, но что если мы не хотим регистрировать свои объекты через registerObject:withName:? Можно ли создавать их прямо в Scheme? Можно. Для этого всего-навсего понадобилось написать процедуру objc-class, который автоматически регистрирует класс и возвращает его.
(objc-class "NSString")
Теперь мы можем создавать объекты!
(-> (-> (objc-class "NSFileManager") "alloc") "init")
Расшифровываю: (objc-class "NSFileManager") возвращает класс NSFileManager, потом мы посылаем ему сообщение alloc, которое возвращает новый объект и этому объекту посылаем init, то есть как если бы мы сделали [[NSFileManager alloc] init].
В objc.scm есть хэлперы для упрощения этого (функция new, например). Смотрите:
(let ((fm (new "NSFileManager")))
(log "App folder name is" (-> fm "displayNameAtPath:" "/Applications")))
покажет название папки /Applications (локализованное).
Все это пока экспериментально, многого нет (например, хотелось бы как-то забайндить Scheme-процедуры и ObjC-методы, сделать какой-нибудь нормальный memory management), но очень интересно в плане изучения… внутренностей Objective-C. И это я еще не выучил Scheme :)
Репозиторий: http://github.com/dchest/tinyscheme/
PS Пока писалась заметка, родилась идея другого синтаксиса посыла сообщений: вместо (-> object "doWithOne:andTwo:" 1 2) сделать (-> object "doWithOne:" 1 "andTwo:" 2). Надо подумать.
—
08.12.2009