Наши партнеры

UnixForum





Библиотека сайта rus-linux.net

Компилятор Glasgow Haskell

Глава 5 из книги "Архитектура приложений с открытым исходным кодом", том 2.

Оригинал: The Glasgow Haskell Compiler
Авторы: Simon Marlow и Simon Peyton-Jones
Перевод: Н.Ромоданов

5.6. Разработка компилятора GHC

Компилятор GHC является отдельным проектом с двадцатилетним сроком существования, при этом он все еще находится в состоянии постоянных инноваций и разработки. По большей части наша инфраструктура разработки и инструментальные средства являются обычными. Например, мы используем треккер ошибок (Trac), вики (также Trac) и Git в качестве средства контроля версий. Такой контроль версий сначала осуществлялся чисто вручную, затем - CVS, затем - Darcs, пока, наконец, в 2010 году мы не перешли на Git. Есть несколько моментов, которые, возможно, менее обычны, и мы их здесь рассмотрим.

Комментарии и замечания

Одна из наиболее серьезных трудностей в большом долгоживущем проекте — это поддержка технической документации в актуальном состоянии. У нас нет серебряной пули, но мы предлагаем один малотехнологичный прием, который послужил нам особенно хорошо: замечания Notes.

При написании кода, часто возникает момент, когда осторожный программист захочет мысленно сказать что-то вроде следующего: «У этого типа данных есть важный инвариант». Он сталкивается с двумя вариантами, которые оба ему не нравятся. Он может добавить инвариант как комментарий, но из-за этого определение типа может стать слишком длинным так, что трудно будет понять, что является конструкторами. Кроме того, он может документировать инвариант в другом месте, и есть риск, что данные устареют. За двадцать лет устареет все!

Исходя из такой мотивации мы разработали следующее очень простое соглашение:

  • Комментарии каких-либо значительных размеров не интегрируются в код, а вместо этого они записываются отдельно с заголовком стандартного вида следующим образом:
    Note [Equality-constrained types]
      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
      The type   forall ab. (a ~ [b]) => blah
      is encoded like this:
    
         ForAllTy (a:*) $ ForAllTy (b:*) $
         FunTy (TyConApp (~) [a, [b]]) $
         blah
    
  • В то место, к которому имеет отношение комментарий, мы добавляем краткий комментарий со ссылкой на замечание:
    data Type
       = FunTy Type Type -- See Note [Equality-constrained types]
    
       | ...
    

    Комментарий указывает, что имеется нечто интересное, и дает точную ссылку на комментарий, в котором это объясняется. Звучит тривиально, но точность выше по сравнению с нашей предыдущей привычкой указывать «смотрите комментарий выше», поскольку часто не ясно, о каком из многочисленных комментариев, указанных выше, ведется речь, причем через несколько лет комментарий может оказаться даже не выше (он может быть ниже или совсем исчезнуть).

Мало того, что можно перейти от кода, в котором указывается ссылка на замечание, к самому замечанию, возможно также и обратное, что тоже часто бывает полезным. Кроме того, на одно и то же замечание можно ссылаться из нескольких мест в коде.

Это простая методика, использующая только код ASCII без автоматизированной поддержки, преобразовала нашу жизнь: в компиляторе GHC есть около 800 замечаний и их количество растет с каждым днем.

Как поддерживать рефакторинг

Код компилятора GHC создается так же быстро, как это было десять лет назад, если не быстрее. Нет сомнения в том, что за этот период времени сложность системы возросла многократно; мы ранее рассказывали об общем количестве кода в компиляторе GHC. Тем не менее, система остается управляемой. Мы связываем это с тремя основными факторами:

  • Ничего не заменит хорошей инженерии программного обеспечения. Модульность всегда окупается: когда между компонентами создается интерфейс API как можно меньшего размера, то это делает отдельные компоненты более гибкими, потому что у них меньше взаимозависимостей. Например, благодаря типу данных Core{} компилятора GHC взаимозависимость между проходами из Core в Core настолько малой, что отдельные проходы становятся почти полностью независимы и их можно выполнять в произвольном порядке.
  • В сравнении с разработка в строго типизированном языке, рефакторинг выглядит просто легким ветерком. Всякий раз, когда нам нужно изменить тип данных или изменить количество аргументов или тип функции, компилятор немедленно сообщает нам, что в других местах в коде нужны исправления. Просто есть абсолютная гарантия того, что статически будет исправлен большой класс ошибок и будет сэкономлено огромное количество времени, и, в особенности, при выполнении рефакторинга. Страшно представить, какое количество вручную написанных тестов нам потребовалось для того, чтобы обеспечить такой же уровень покрытия кода, который обеспечивает система типов.
  • При программировании в чисто функциональном язык, трудно представить случайные зависимости, связанные с состояниями. Если вы решите, что вам вдруг понадобится доступ к некоторому состоянию глубоко в алгоритме, то в императивном языке вы могли бы испытать желание просто сделать это состояние глобально видимым, а не явно передавать его туда, где в нем есть необходимость. Такой способ, в конечном итоге, приводит к сплетению невидимым зависимостей и к хрупкому коду: коду, который легко можно испортить при модификации. Чисто функциональное программирование заставляет вас делать все зависимости явными, что оказывает некоторое негативное давление на добавление новых зависимостей, а меньшее количество зависимостей означает большую модульность. Конечно, когда необходимо добавить новую зависимость, то для того, чтобы выразить зависимость, чистота подхода заставит вас написать больший объем кода, но, на наш взгляд, это та цена, которую стоит заплатить за долгосрочное качество кода.

    А в качестве дополнительного преимущества, чисто функциональный код является при сборке потокобезопасным и его, как правило, легче распараллеливать.

Отступления от правил

Когда мы оглядываемся назад на те изменения, которые нам потребовалось сделать для того, чтобы проект GHC вырос таким образом, нам становится очевиден общий урок: когда мы отходили от чистой функциональности, будь то в целях эффективности или удобства, то, как правило, это впоследствии приводило к негативные результатам. Этому у нас есть несколько крупных примеров:

  • В компиляторе GHC используется несколько структур данных, в которых есть внутренние преобразования. Одной из них является тип FastString, в котором присутствует единая глобальная хэш-таблица, а другой структурой является глобальный кэш NameCache, с помощью которого гарантируется, что всем внешним именам будут присвоены уникальные номера. Когда мы попытались распараллелить работу компилятора GHC (то есть, сделать, чтобы компилятор GHC на многоядерном процессоре компилировал нескольких модулей параллельно), то эти структуры данных, в которых использовались внутренние преобразования, оказались единственным камней преткновения. Если бы мы в этих местах не прибегали к внутренним преобразованиям, то распараллеливание компилятора GHC было бы почти тривиальным.

    Хотя мы действительно собрали прототип параллельной версии компилятора GHC, на самом деле в настоящее время в компиляторе GHC нет поддержки параллельной компиляции, но это в значительной степени потому, что мы не затратили усилия, необходимые для того, чтобы сделать эти изменяемых структуры данных поточно-ориентированными.

  • Поведение компилятора GHC регулируется в значительной степени флагами командной строки. По определению эти флаги командной строки являются неизменными в течение определенного времени работы GHC, поэтому в ранних версиях компилятора GHC мы сделали значения этих флагов доступными в виде констант верхнего уровня. Например, был флаг верхнего уровня opt_GlasgowExts, имеющий тип Bool, с помощью которого указывалось, должны ли использоваться определенные расширения языка или нет. Константы верхнего уровня очень удобны, т.к. там, где к ним в коде нужен доступ, их значения не требуется явно передавать в виде аргументов.

    Конечно, эти параметры в действительности не являются константами, поскольку при разных запусках компилятора они могут отличаться, и в определении opt_GlasgowExts есть вызов unsafePerformIO, который скрывает побочный эффект. Как бы это не было, но этот трюк обычно считается «достаточно безопасным», поскольку это значение остается постоянным при любом конкретном запуске; например, это не мешает при компиляции использовать оптимизацию.

    Однако позже компилятор GHC был преобразован из одномодульного компилятора в компилятор, состоящий из нескольких модулей. В этот момент трюк с использованием для флагов констант верхнего уровня стал неприемлемым, поскольку при компиляции различных модулей флаги могут иметь различные значения. Так что нам пришлось преобразовывать большой объем кода для того, чтобы значения флагов передавать явным образом.

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


Продолжение статьи: Разработка системы RTS