Re[7]: Недостатки Nemerle
От: hi_octane Беларусь  
Дата: 10.07.12 08:03
Оценка: 394 (13) +1
I>А что конкретно из макросов пригодилось, ну, кроме логирования ?

Там две итерации было. В первой (2007-2008г) мы только учились, поэтому там было буквально пара макросов, остальное врукопашную, C# style and flow, только с упором на паттерн-матчинг и ФП.

Вторая итерация была сильно позже (2009г) и умели мы гораздо больше, но понимали всё ещё столько же . Ну правильно, макросов же ещё не писали толком. А главное, пока было оплачиваемое время между первой и второй итерациями, — лепили фреймворк. И не то чтобы он оказался совсем не нужен, просто наше представление о программирование было всё ещё сильно искажено призмой C# и даже C++.

Вот пример — подметили что многие (да наверное все) объекты, подерживающие IDisposable, делают Dispose вложенным объектам, что, в принципе, логично. Для этого придумали аттрибут AutoDispose, который должен был использоваться примерно так:
[AutoDispose]
class A : IDisposable
{
  [AutoDispose]
  private b : B;

  [AutoDispose]
  private c : C;
}

Ну и типа генерируется православный метод метод Dispose, который там Dispose(disposing : bool), GC.SuppressFinalize, и вложенным объектам тоже Dispose если надо, в общем всё хорошо. Макрос был весело написан, и через неделю использования оказалось что есть один нюанс — эти [AutoDispose] не нужны как тот скрипач. В итоге всё выродилось до полностью автоматической реализации IDisposable, без всяких аттрибутов вообще. Ориентиром было наличие самого интерфейса IDisposable. Т.е. если класс заявлял что он IDisposable, но не давал самописной реализации своего Dispose, то создавалась реализация "по-умолчанию" для всех приватных и протектед полей. Оказалось удобно и надёжно, писанины меньше, меньше места для ошибок.

Второй пример, мы лепили хитрый макрос, для особых полей и свойств, чтобы их случайно нельзя было изменить откуда-то, откуда нельзя. При этом существовала группировка по строковым ключам. Идея была такой:
class ACollection : IList
{
  [ModifcationGroup("IList")]
  public Count : int { get; protected set; }

  [ModifcationGroup("IList")]
  public Add(value : object) : int
  {
  }
}

Ну и типа только методам из одной группы можно менять поля/свойства из этой группы. Красиво, но быстро надоело, и как только столкнулись с этим в реальной жизни, заменили на простецкий аттрибут:
class ACollection : IList
{
  [ChangedBy(Add, Remove, Clear)]//это имена методов, которым разрешено менять Count
  public Count : int { get; set; }
}

Теперь если кто-то попытается изменить Count в каком-то другом методе — получает ошибку времени компиляции. Если какого-то метода нет — тоже ошибка компиляции. Для чтения Count даёт лок-фри кэшированное значение. Главное декларативно и сразу видно кто может лазить в Count. Жалко только goto definition для этой штуки не работал — с Location мы как-то не разобрались в тот раз. Посмотрим насколько проще с этим будет в N2

Или вот слепили мы хитрые методы для синхронизации, там можно было в нашей реализации lock перечислять объекты через ",". Кроме того была секция то-ли late, то-ли reserve, в которой можно было перечислить что ты собираешься лочить потом, после того как залочишь свой объект. Это было придумано для того чтобы обеспечить гарантированно без-деадлочную блокировку. Обеспечили, а оно оказалось втопку. Зато его место заняли несколько макросов вида readlock(object), writelock(object). Вот только был маленький секрет — эти макросы лочили не сам объект, а его SyncRoot, причём если у объекта не было своего SyncRoot, но ему в конструктор передавался другой объект(родитель), со своим SyncRoot, то лочился родительский SyncRoot, ну и так в цикле, пока у кого-нибудь не найдётся SyncRoot. Самое главное что если SyncRoot был object — то макросы превращались в обычнй Monitor.Enter. А вот если SyncRoot был ReaderWriterLock — то тогда в вызовы его методов. А если SyncRoot был ReaderWriteLockSlim, то ещё проверялось на то, держим мы уже лок или нет, потому как ReaderWriterLockSlim не поддерживает реентерабельность. Да, и во избежание ошибок мы библиотечный lock подменили на наш write_lock. Магии много, но пока она незаметная и её не надо держать в голове — кода рукопашного мало, а значит пользоваться удобно и накосячить сложно.

Кроме того много работы выполняли макросы и стиль программирования, за которые в C# проекте я бы руки отрывал. Например нужно было устанавливать связь с разными старыми системами, написанными чёрт знает на чём. Для каждой такой системы был заведён свой проект, с одинаковой иерархией namespace'ов. И в них была строгая иерархия наименования типа Root.Api.XyzTasks, Root.Api.XyzSerializer, Root.Comm.XyzListener и т.п. Так вот если в имени класса было Xyz, то автоматом генерировались пачками приватные члены, характерные для этой системы. Если при этом ещё и namespace Comm, то создавалась прокся с полностью асинхронными вызовами тех public методов что имелись в классе. Ну и так далее.

Или вот ещё ацкий трэш — у нас некоторые исключения имели помимо важности ещё свойство Color, которым (ааа111!!!) их надо было подсвечивать в GUI. Выставлялось это свойство автоматом, исходя из того в каком классе, в процессе работы над какой задачей, случился throw. Почему такое нарушение принципов вообще оказалось возможно — а потому что оно не выставлялось вручную вообще нигде. Т.е. GUI код просто использовал это свойство, и ему по-барабану как оно возникло, а программист который исключение кидал — даже не знал про какое-то там свойство, и его не выставлял. Соотвественно если бы мы решили эту хрень заменить, или вообще убрать — надо было бы менять только одно место, а значит принципы правильного разделения BL и GUI можно слать в конкретно этом случае лесом.

Очень полезными оказались прекомпилируемые регекспы, которые были быстрые как компилируемые, и описанные прямо в коде. Кроме того у Nemerle есть свой match/биндинг по регекспам (называется кажется rx match или regex match), и мы по образу и подобию слепили себе такой же, но почему и чем он был лучше — уже не помню.

Множественное наследование оказалось не нужно вовсе. traits на интерфейсах оказалось что использовались только для сериализации, больше впихнуть было некуда, а сериализация была на лямбдах удобнее. Так и стали traits зомбями nUnit'ными.

Ну и логгирование конечно сказочное. Вообще если начинать изучать фишки макропрограммирования — то советую это делать с логгирования. Такие штуки как имя и параметры падающего метода, об которые в C#-форумах раз в месяц копья ломают, время исполнения, автоматическое заполнение полей исключения всякими там контекстами, и важностями, сохранение дампа если бросается исключение типа AbsolutelyCriticalFatalDeadlyUnhandledNeverThrowThisException, асинхронное логгирование в отдельном потоке в базу, причём строго после того как метод завершится, чтоб не влиять на время исполнения — вообще не вопрос.

И ещё — оказались очень нужны и важны макросы которые не добавляют функционал, а наоборот ограничивают использование чего-то откуда не планировалось, причём во время компиляции. Например нельзя лезть в объекты неймспейса GUI из объектов неймспейса Threading или Comm. Или нельзя приводить какой-то интерфейс больше ни к какому другому интерфейсу (полагаясь на тайное знание о том что там за интерфейсом какой-то конкретный объект). Это позволяет очень сильно снизить число ошибок причём без единого теста или захода в отладку. Таких штук мы вообще не задумывали в фреймворке, а потом использовали повсеместно.

А теперь ДЗЕН вынесенный из этого проекта:
Ближе к концу мы уже не стремились лепить библиотечные навороты и расширять фреймворк, а скорее старались заменять макросами рукопашный код насовсем но локально. Т.е. гораздо проще разработать какую-то феньку нужную в 5 местах и без параметров, и какую-то очень похожую феньку (даже внешне точно такую-же) для 5 других мест, чем лепить универсальную, конфигурируемую через кучу параметров мега-фень, способную покрыть все эти 10 мест и ещё 20 гипотетических похожих.

Ну и экономический момент. Ожидая проблем с отладкой макросов и самого проекта в котором будет много макро-магии, я сознательно завысил оценку времени на отладку раза в 3. Причём первый раз завысил, руководствуясь тем что язык новый, и тогда (2007-й год), интеграция ещё работала очень слабо, и некоторые штуки было проще написать в Far+Colorer чем в студии. А второй раз — завысил исходя из того что всю эту макро-магию потом отладчиком разбирать будет сложно. Оба раза зря.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.