October 3rd, 2011

glider

Макросам быть! Часть 2, конкретика.

Часть 1, лирика
Часть 2, конкретика.

После нескольких обсуждений в рамках проекта "Кеплер" на свет появились альфа-версии трех сидов (SID = scala improvement document): SID 100: Lightweight macros, SID 101: Quasi-quotations, SID 102: Macro types. В ближайшее время мы доведем до ума и запостим эти сиды (+ еще парочку интересных улучшений для Скалы (!)) в мейлинг листе для публичного обсуждения, но я просто не смог удержаться от того, чтобы рассказать о них прямо сейчас. Как обычно, дисклеймер: сабжевые сиды сугубо экспериментальны и могут быть полностью изменены или выброшены на свалку прямо завтра, но есть хороший шанс, что они окажутся в транке, если пройдут испытание временем и Мартином.

Сегодня нашим юзкейсом станет LINQ - широко известная в узких кругах технология виртуализации запросов, а таже окружающая ее инфраструктура. Мы хотим иметь возможность написать db.Products.filter(p => p.Name.startsWith("foobar")) да причем так, чтобы db превратила наш запрос в SQL и заодно проверила типизацию этого выражения относительно нашей базы данных.

Так как в заголовке поста написано "макросы", все будем делать макросах и посмотрим, насколько далеко мы сможем продвинуться. Если вы с макросами не знакомы, рекомендую зачесть мини-введение, в котором на примерах рассказывается про базовые понятия макрологии. Итак, не будем тянуть кота за хвост, вот реализация метода filter:

class Queryable[T, Repr](query: Query = null) {
  macro filter(p: T => Boolean): Repr = p match {
    case <[ _ => ${ret: Boolean} ]> if ret == <[ true ]> => <[ $this ]>
    case _ => <[
      val b = $this.newBuilder
      b.query = Filter($this.query == null ? $this : $this.query, $reify(p))
      b.result
    ]>
  }
}

class Product { ... }
class Products extends Queryable[Product, Products](connectionString: String) { ... }
val products = new Products(“Server=127.0.0.1;Database=Foo;”)
products.filter(p => p.Name.startsWith("foobar"))
Посмотрите как здорово макрос интегрируется в язык! Объявляется он в точности как обычный скаловский метод, вызывается точно так же, внутри макроса тоже все как обычно - у нас есть доступ к параметрам, к неявным переменным вроде this (имплициты тоже поддерживаются). Забегая вперед, компилируется макрос тоже в обычный метод, причем с минимальными изменениями. Кроме того, благодаря квазицитатам и гигиене мы можем писать генерируемый код как есть и не заморачиваться с темплейт энжинами или наколенным сбором деревьев выражений.

В дополнение к собственно языковой виртуализации у нас есть интересная идея на тему уменьшения количества кода, генерируемого для статически типизируемого доступа к внешним датасорсам (как можно догадаться, эта идея вдохновлена недавно продемонстрированными тайп-провайдерами из F#). С помощью макросов мы можем без текстовой генерации кодяры и без хардкодов в компиляторе предоставить программистам возможность делать вот так:
macro type MySqlDb(connectionString: String) = ...
val fooDb = MySqlDb(“Server=127.0.0.1;Database=Foo;”)
class BarDb extends MySqlDb(“Server=127.0.0.1;Database=Bar;”)
После этого можно писать все те же fooDb.Products.filter(p => p.Name.startsWith("foobar")), но уже без необходимости явно объявлять класс Product. Сабжевый класс, вместе с другими классами, которые соответствуют другим таблицам в базе, будет сгенерирован макросом MySqlDb. Причем в силу того, что все построено на макросах, для кодогенерации у нас будет все тот же API, который используется внутри компилятора. В случае необходимости дофигачить кастомные методы или поменять шаблон генерации класса в заранее не предусмотренную сторону у программиста полностью развязаны руки. Наконец, генерируемый класс может даже захватывать и использовать переменные из лексического скоупа (в том числе и имплициты!).