Пейтон-Джонс: Верно. Чтобы я мог написать контракт для функции, например такой: при задании аргументов со значением выше ноля функция дает результат меньше ноля.
Сейбел: Как вы подходите к проектированию программ?
Пейтон-Джонс: Наверное, я могу сказать, что главная проблема — скажем, если я пишу что-то новое для GHC, — не в том, чтобы облечь идею в код, а скорее в том, чтобы сформулировать идею.
К примеру, мы сейчас в середине большого рефакторинга части компилятора, отвечающей за генерацию кода. Сейчас в компиляторе есть этап, когда он фактически берет функциональный язык и преобразует его в С—, язык императивный. Это важный шаг. С— называется так, потому что он похож на подмножество Си, но на самом деле он задумывался как портируемый язык ассемблера. Он не записывается ASCII-символами, это просто внутренний тип данных. Поэтому на этом этапе компилятор выполняет функцию преобразования структуры данных, представляющей функциональный язык, в структуру данных, представляющую императивный язык. Как совершается этот шаг?
Сейчас за это отвечает довольно сложный фрагмент кода. Но недавно я понял, что задача раскладывается на две части: 1) преобразование в диалект С--, позволяющий делать вызовы процедур, когда внутри одной процедуры можно вызвать другую, и 2) преобразование этого в подъязык без всяких вызовов, кроме хвостовых.
Затем главное понять, что здесь является типом данных. Что у нас в С--? Структура данных, представляющая императивную программу. Второй шаг — пройти по программе, смотря на каждый кусочек в отдельности. Ваше внимание идет по пути управляющей логики программы или, наоборот, откатывается назад через нее. Это удобно представлять через структуру данных под названием Zipper — полезная, чисто функциональная структура данных для того, чтобы окинуть взглядом чисто функциональную структуру данных.
Норман Рэмзи из Гарварда нашел способ использовать ее для перемещения по структурам данных, представляющим императивные управляющие графы. Мы с ним и Джоном Диасом с этой целью перепроектировали выходную часть GHC с применением этой технологии. И теперь мы можем использовать тот же самый бэкенд для других языков.
Многие споры шли на уровне типов. Норман говорил: «Вот API», — показывая сигнатуру типов, а я в ответ: «Зачем так сложно?» Он объяснял зачем, а я говорил: «Может быть, вот так будет проще». Так что мы довольно долго бились над описанием типов.
Но много времени уходило не на собственно программирование, а на определение самой идеи. Что мы хотим сделать с анализом потока данных? Надо было дать четкий ответ: что подразумевает такой-то шаг программы. Так что немало времени мы потратили на уточнение того, что у нас на входе и что на выходе, и какие у них типы данных. И всего лишь определив эти типы данных, мы довольно подробно описали работу программы. Даже удивительно, насколько подробно.
Сейбел: Как размышления над типом данных соотносятся с кодированием? Набросав типы, вы можете приниматься за код? Или, наоборот, написание кода помогает в понимании типов?
Пейтон-Джонс: Скорее последнее. Я сразу начинаю писать сигнатуры типов в файл. Даже скорее я начинаю писать код, работающий со значениями этих типов. Потом возвращаюсь к типам и изменяю их. Этот процесс не делится четко на два этапа, когда определил типы и садишься за код.
Пожалуй, в этом смысле мне не хватает дисциплины, так как я ни разу не работал в большой команде. Работая в одиночку, можно позволить себе делать вещи в расчете на то, что они помещаются в твоей голове, что, возможно, не получится в большой команде.
Сейбел: Вы говорили о том, что при последней перетряске кодов в GHC его компоненты стали намного более универсальными. GHC — большая программа, которая эволюционировала со временем, так что вы смогли воспользоваться универсальностью, но и заплатили за ее избыток. Что вы узнали о том, как балансировать между избытком и недостатком универсальности?
Пейтон-Джонс: Я предпочитаю вообще не писать чего-то очень универсального. Стараюсь сделать свои программы как можно более красивыми, но не обязательно универсальными. Это разные вещи. Я стараюсь, чтобы код выполнял свою задачу максимально ясным и четким способом. И лишь когда обнаруживаю, что уже писал этот код, то спохватываюсь: зачем делать это снова, достаточно сделать это в одном месте, добавив аргументы, чтобы параметризовать несовпадающие участки.
Сейбел: Какие среды и инструменты вы сейчас используете?
Пейтон-Джонс: О, ужасно примитивные. Работаю в Emacs, компилирую при помощи GHC. Вот и все. Есть профилирующие инструменты, поставляемые с нашим компилятором; люди часто пользуются ими, чтобы профилировать программы на Haskell. Мы применяем их для профилирования самого компилятора. GHC производит много промежуточной выходной информации, и мне видно, что там происходит.
Отладка для меня часто связана с тем, что компилятор порождает плохой код, и я изучаю состояние его внутренностей. Или вот: взять небольшую исходную программу, скомпилировать до такого-то места, посмотреть — вот что такое для меня отладка. Я редко прохожу программу пошагово, чаще всего гляжу на значения разных частей скомпилированного кода.
Я даже нечасто пользуюсь всеми хитростями Emacs, хотя некоторые любят этим заниматься. Также масса народу пользуется интегрированными средами разработки — Visual Studio, Eclipse. Мне кажется, неприятие языков функционального программирования отчасти связано с тем, что мы не выпустили свою интегрированную среду разработки. Опять проблема курицы и яйца. Сейчас напирают на курицу — идет всплеск интереса к функциональному программированию. Надеюсь, что и за яйцо тоже возьмутся. Интегрированная среда для Haskell потребует серьезной разработки. Даже при таких оболочках, как Visual Studio или Eclipse, предстоит большая работа над красивым плагином, который бы делал все как надо.
Сейбел: В GHC есть цикл REPL, GHCI. Вы предпочитаете работать с Haskell интерактивно?
Пейтон-Джонс: Ну, сам я сейчас в основном редактирую и компилирую. Но другие просто живут в GHCI.
Сейбел: Когда дело доходит до тестирования, у функциональных языков есть один плюс: если требуется протестировать небольшую функцию, сидящую в глубине программы, надо просто выяснить, что она принимает на входе.
Пейтон-Джонс: Думаю, если входные данные достаточно просты, проблем с моей программой быть не должно. Проблемы возникают, когда GHC пытается скомпилировать какую-нибудь непомерную входную программу и получает неверный ответ.
Тестирование необычайно важно для создания свойств. Очень полезна QuickCheck — библиотека Haskell, генерирующая случайные тесты для функции в зависимости от ее типа. И я старался понять, почему использую QuickCheck — очень приятный инструмент — меньше, чем мог бы. Видимо, потому, что меня беспокоят ситуации, когда трудно сгенерировать тестовые данные. Так или иначе, куча народу создает программы, от которых GHC просто воротит. Для этого у GHC и есть свой багтрекер.
Я обычно начинаю с чего-нибудь, что работает не так. Возможно, компилятор зависнет в каком-то месте, или откажется выполнять программу, которую должен выполнять, или станет генерировать неоптимальный код. Если он просто генерирует плохой код, я гляжу на него на разных стадиях компиляции и соображаю: «Здесь все в порядке, здесь тоже, а здесь нет — в чем дело?»
Сейбел: Как именно вы это делаете?
Пейтон-Джонс: В GHC есть флажки, которые позволяют выводить что-либо на печать.
Сейбел: Встроенные операторы печати для отладки?
Пейтон-Джонс: Да. Плюс к тому структура такая же, как у большинства компиляторов: на верхнем уровне есть конвейерная структура преобразований. Если что-то не так внутри какого-то из шагов, задача усложняется. Но я предпочитаю несложные методы отладки. Покажите мне программу до и после данного шага. Ага, я вижу, в чем ошибка! Если же не вижу, то могу использовать какой-нибудь из небезопасных операторов printf, чтобы понять, что происходит.
Есть разные отладчики для Haskell. Один из них, и просто отличный, написал в этом году студент летней школы, Пепе Иборра: это интерактивный отладчик, который теперь поставляется вместе с GHC. Я, правда, его мало использовал — он появился недавно, и, кроме того, не очень понятно, как пошагово проходить функциональную программу.
Были любопытные исследования насчет отладки функциональных программ. Жаль, что у нас нет простого и очевидного решения, но зато это интересная исследовательская проблема.
Я все это говорю, чтобы показать, что использую крайне примитивные техники отладки, например через небезопасные операторы printf. Тут нечем гордиться. Но долгое время ничего больше не было — по крайней мере, если брать GHC. Я выработал способы, которые делают для меня этот путь самым коротким.
Сейбел: Это как всегда. Зачем создавать новые отладчики, если люди довольствуются операторами печати?
Пейтон-Джонс: Это скорее культурное явление. Если перейти на отладчики платформы .NET, на которые потрачены десятки и сотни тысяч человекочасов, думаю, результат будет качественно иным. Вероятно, для хорошей работы отладчики требуют еще больше циклов разработки. Но зато в итоге получается образцово полезная вещь.
Возможно, вы разговаривали в основном с людьми академического склада и с теми, кто в силу возраста не привык к сложным отладчикам. Я бы не стал делать никаких общих выводов. И, конечно, не хочу принизить значение качественных отладчиков — особенно для сложных систем с множеством программных слоев. GHC очень прост сравнительно со средой .NET, где есть слои DOM и UML, и не знаю, что еще. Теперь вокруг столько примочек, что программная поддержка становится действительно важной.
Сейбел: Еще один способ создавать правильные программы — формальные доказательства. Что вы думаете об их полезности?