?

Log in

No account? Create an account

Макросы vs шаблоны - Excelsior

Aug. 29th, 2013

10:24 am - Макросы vs шаблоны

Previous Entry Share Next Entry

Продолжая дискуссию про метапрограммирование в D: http://thedeemon.livejournal.com/68456.html.

С одной стороны, дишный стиль МП кажется очень адхочным, но, с другой стороны, поверхностное знакомство и отзывы автора журнала оставляют впечатление чего-то крайне легкого в использовании. Цитируя уважаемого thedeemon:

Я видел много разных попыток сделать удобные лисп-стайл макросы (camlp4, nemerle, haxe, какие-то кусочки скалы...), и везде это пляски с бубном, часто отдельные фазы компиляции, отдельный синтаксис и длинное страшное слово "метапрограммирование". В D же просто берешь и пишешь generic код, даже и мысль о таком длинном слове не приходит.
Вот, например, надо сгенерировать класс, который в зависимости от типа, передаваемого в генератор, будет иметь или не иметь какие-то мемберы [1].

Макросы

В скале мы пишем макрос, который заворачивает шаблон класса в квазицитату. В эту квазицитату мы сплайсим условно генерируемые кусочки. Например, вот так:
val length = if (R.method("hasLength").exists) q"..." else EmptyTree
val empty = if (isInfinite(R)) q"..." else EmptyTree
q"""
  class MyRange {
    val innerRange: R = ...
    ... // code that exists in all instantiations of MyRange
    $length
    $empty
  }
"""
Да, классно, что есть полноценный рефлекшен API вместо пачки захардкодженных в язык __traits и основанных на них самописных темплейтах, которые, например, делают is(typeof(...)). Да, классно, что сниппеты кода являются первоклассными сущностями и что, соответственно, нет проблем их модуляризировать как угодно. Да, классно, что МП реализуется только макросами, а не кучей языковых фич вроде alias, enum, template, и так далее [2, 3].

Шаблоны

Но все же, посмотрите, как задорно выглядит дишный вариант! (Ниже приведена моя личная экстраполяция примера из книжки, может содержать ошибки). Заметьте отсутствие ада метауровней вида q"бла" бла бла q"бла бла" бла q"бла" (что-то похожее мы обсуждали на scala-internals в контексте тайп макросов [4]). Понятное дело, метауровни никуда не делись - они просто перемешались с генерируемым кодом, но насколько стало удобнее читать:
struct MyRange(R) {
  R innerRange;
  ... // code that exists in all instantiations of MyRange
  static if (hasLength!R)
    auto length() { return innerRange.length; }
  static if (isInfinite!R)
    enum bool empty = false;
}

Вопрос

А теперь вопрос. Какой стиль метапрограммирования кажется вам предпочтительным для решения повседневных задач: макросы или темплейты? Если ответ неоднозначный, то в каких случаях вы бы выбрали макросы, а в каких - темплейты?

Предлагаю оставить в стороне проблемы имплементации того или иного стиля (падения компилятора на сложных темплейтах, сырость первой (экспериментальной!) версии макросов в Скале 2.10, и так далее). Предположим, что подлежащие реализации работают идеально без глюков.

Также хочу отметить, что не принимается ответ "макросы круче потому, что на них можно реализовать темплейты". Вопрос не в том, какой фундамент для МП теоретически более элегантный, а в том, какой тул вам было бы приятнее использовать, независимо от того, на чем именно этот тул реализован.

Ссылки

[1] https://github.com/PhilippeSigaud/D-templates-tutorial/raw/master/D-templates-tutorial.pdf, страница 21, раздел "Optional code"
[2] http://dicebot.blogspot.ch/2013/04/7-trivial-tips-for-compile-time-meta.html, часть 1
[3] http://xeno-by.livejournal.com/86259.html
[4] https://groups.google.com/d/msg/scala-internals/91W0-PxMQ9Q/hq5n47RxfN8J

Tags: ,

Comments:

[User Picture]
From:moi_drug_daun
Date:August 29th, 2013 11:14 am (UTC)
(Link)
Отдам свой голос.
С точки зрения аматора Дишные шаблоны выглядят наглядней и аккуратней.
(Reply) (Thread)
[User Picture]
From:wizzard0
Date:August 29th, 2013 12:46 pm (UTC)
(Link)
+1 к наглядности и аккуратности
(Reply) (Parent) (Thread)
[User Picture]
From:andy128k
Date:August 29th, 2013 01:10 pm (UTC)
(Link)
макросы круче потому, что на них можно реализовать не только темплейты
(Reply) (Thread)
[User Picture]
From:akuklev
Date:August 29th, 2013 03:21 pm (UTC)
(Link)
Вопрос как всегда в том, что у нас дефолтный уровень и что мы эскейпим. Дефолтным уровнем всегда должно быть то, чего больше, что более по-существу важно при чтении. А это в зависимости от задачи может быть как метакод, так и рантаймкод. В теле макроса дефолтный уровень код макроса, а генерируемый код инклюдится в виде квазицитат. В темплейтах наоборот дефолтный уровень target-код, а метакод эскейпится словом static. Шош тут делать, разные задачи элегантнее делать по-разному. Ту, что ты привёл выше, темплейтами делать элегантнее. Естественно, тривиальные с точки зрения алгоритмики макросов задачи темплейтами делать кошерно и красиво.
А попробуешь на темплейтах реализовать что-нибудь эдакое, где нужно два вложенных цикла, да рекурсийка, да pattern-matching квазаквотный, и тут немедленно поймёшь, что темплейты для этой задачи непригодны.

Imho, no silver bullet here, приучаем пользователей к тому что в любой задаче с двумя и более метауровнями, (от стейтфул-кода через dataflow и до макросов) на простых задачах кошерно использовать level inversion (do notation/idiom brackets, дишные темплейты с эскейп-маркером статик, это всё одно и то же), а на сложных делать пуро и сверху-вниз.
(Reply) (Thread)
[User Picture]
From:atlanter
Date:August 29th, 2013 04:15 pm (UTC)
(Link)
Согласен. Короче, как обычно, нужно сделать и то и другое :)
(Reply) (Parent) (Thread)
From:Valentin Budaev
Date:August 29th, 2013 04:17 pm (UTC)
(Link)
Это все вопрос исключительно выразительности макросистемы. Если можно раскрываться в макросы и квазицитаты, то можно сделать альтернативные макро-объявления, в которых дефолтным уровнем будет уровень рантайма.
(Reply) (Parent) (Thread)
[User Picture]
From:thedeemon
Date:August 29th, 2013 05:16 pm (UTC)
(Link)
D не одним static if богат, там и "два вложенных цикла, да рекурсийка, да..." так же просто делаются. TypeTuple, раскрываемый в статике foreach по ним, рекурсивные шаблонные ф-ии и исполнение почти произвольного чистого кода при компиляции - это вам не старинные плюсовые темплейты.
(Reply) (Parent) (Thread)
From:M E
Date:August 29th, 2013 08:31 pm (UTC)
(Link)
а что будет, если R *тоже* зависит от каких-то членов или методов MyRange(R) напрямую или через цепочку типов? (в шаблонах плюсов у нас порядок в данном случае задается вручную (путем включения куда-то forward declaration, а куда-то полных деклараций), а в Д как?)
(Reply) (Parent) (Thread)
[User Picture]
From:thedeemon
Date:August 30th, 2013 04:54 am (UTC)
(Link)
А можно пример желаемого на псевдокоде?
(Reply) (Parent) (Thread)
From:M E
Date:August 31st, 2013 06:14 pm (UTC)
(Link)
пример высосан из пальца (в смысле, я не буду рассказывать, зачем такое может потребоваться на практике)

пусть у нас есть классы X и Y

нам нужно, чтобы при наличии в классе X поля x в классе Y появлялись определенные поля и метод foo, а при наличии в классе Y поля y в классе X появлялись определенные поля и метод bar

при этом очень желательно, но не обязательно, указать что X зависит от Y, а Y зависит от X — полностью это указать не получится, т.к. полная запись будет что-то вроде X<Y<X<Y<......>>>> и никогда не закончится

Edited at 2013-08-31 06:17 pm (UTC)
(Reply) (Parent) (Thread)
[User Picture]
From:thedeemon
Date:August 31st, 2013 07:42 pm (UTC)
(Link)
А, хороший вопрос. Порядок объявлений в D не важен, и вот такой код компилится:
class X {
    int x;
    static if (hasMember!(Y, "y")) 
        bool bar;
}

class Y {
    string y;
    //static if (hasMember!(X, "x")) 
    //	byte foo;
}

Но если раскомментировать вторую проверку, то перестает, не осиливает такую явную циклическую зависимость. Возможно, какой-нибудь трюк с наследованием или хитрыми шаблонами поможет, но сходу не назову.
(Reply) (Parent) (Thread)
[User Picture]
From:VladDQ
Date:November 2nd, 2014 01:50 am (UTC)
(Link)
А как насчет файлик прочитать? Или там ХМЛ распарсить? (в компайлтайме, естественно)
(Reply) (Parent) (Thread)
[User Picture]
From:thedeemon
Date:November 2nd, 2014 10:03 am (UTC)
(Link)
Тоже легко.
enum s = import("file.xml"); // compile time string constant

Потом берешь Pegged и парсишь
https://github.com/PhilippeSigaud/Pegged
он в компайлтайме умеет применять только что описанные (в том же файле) грамматики.
(Reply) (Parent) (Thread)
From:M E
Date:August 29th, 2013 08:29 pm (UTC)
(Link)
> а на сложных делать пуро и сверху-вниз

а чуть подробнее?
(Reply) (Parent) (Thread)
From:M E
Date:August 29th, 2013 08:26 pm (UTC)
(Link)
однозначно д-шный подход лучше, т.к. это именно то, что я хотел бы сказать (написать)

ну или что почти то же самое — разница между макросом и static if это разница между грудой печатных плат и устройством, запакованным в корпус

можно чуть изменить формулировку — скажем, потребовать, чтобы MyRange(R) работала даже в том случае, если R *тоже* зависит от каких-то членов или методов MyRange(R) напрямую или через цепочку типов; при этом *в макросе* необходимо будет, например, производить топологическую сортировку и выявлять кольцевые зависимости (куча кода), однако синтаксис static if останется неизменным

декларативность, короче
(Reply) (Thread)
[User Picture]
From:justy_tylor
Date:August 30th, 2013 01:17 pm (UTC)
(Link)
Это частный случай. Для описания полиморфных контейнеров подход D более пригоден. Там уровень абстракции практически не меняется. А на других уровнях абстракции (например, получение кода из датасетов) уже понадобятся внешние генераторы кода или внутренние макросы с эффектами.

Впрочем, модификатор static здесь выглядит как масло масляное. Глобальные декларации и описания типов - они уже compile time. Вероятно, оставили для схожести с мета-включениями внутри функций, чтобы не смущать юных падаванов.
(Reply) (Thread)
[User Picture]
From:VladDQ
Date:November 2nd, 2014 01:41 am (UTC)
(Link)
Ты делаешь не верное противопоставление. Имея макросы мы можем сделать аналог этих самых if-ов и получить тот же код.

Например, мы можем создать макро-аннотацию @Conditional и перепишу твой код так:

struct MyRange[R] {
innerRange : R;
... // code that exists in all instantiations of MyRange
@Conditional(R.length)
auto length() { return innerRange.length; }

@Conditional(R.isInfinite)
enum bool empty = false;
}

Проблема только в том, чтобы Скала предоставляла возможность вмешаться в процесс материализации шаблона. Боюсь, что именно этого она сделать и не позволяет. По крайней мере в Немерле дженерики - это рантайм-сущность, и вмешаться в процесс их материализации не представляется возможным.

Так что обсуждать скорее нужно не путь макросов vs. путь D, а подходы к обобщенному программированию.
(Reply) (Thread)
[User Picture]
From:xeno_by
Date:November 3rd, 2014 08:59 am (UTC)
(Link)
Я согласен, что похожую функциональность можно реализовать на макросах, но в этом посте я также хотел обратить внимание на UI. В ди программист может вовсю использовать концепцию partial evaluation для того, чтобы генерировать код выше естественными конструкциями типа ифа. В случае макросов приходится изобретать что-то новое (например, аннотации), что повышает порог вхождения.

В Скале при помощи тайп макросов (type macros) можно создать тип, каждое использование которого вызывает макрос, который может делать что угодно. Например, инстанциировать шаблон. Это экспериментальная функциональность, которая пока что отключена, но ее возможно включить обратно. Прошу прощения, что не скидываю линк на документацию (у меня было немножко доков, которые можно нагуглить по ключевой фразе type macros) - я сейчас не за компьютером.
(Reply) (Parent) (Thread)
[User Picture]
From:andybil
Date:March 26th, 2015 03:58 pm (UTC)
(Link)
Вот он, конец Кремниевой долины!
"замена x86" - Результатов: 17
(Reply) (Thread)