Сейбел: А что насчет доказательства правильности программ?
Айк: Это трудно. А люди по большей части ленивы. Ларри Уолл прав: лень должна считаться достоинством. Вот почему я предпочитаю автоматизировать этот процесс. Ученые его любят, большинство программистов ненавидят. Писать предикаты утверждений - это может принести пользу. У нас в Mozilla было несколько плохих утверждений - скорее они должны были быть предупреждениями, - но со временем хороших становится все больше. Благодаря этому нас наконец озарило, что есть инварианты - те, которые вы хотели бы реализовать в некоей идеальной системе типов.
Думаю, полезно считать утверждения точками доказательства правильности программы. Но не нужно стремиться к полному доказательству. Сколько дыр в серьезных доказательствах, напечатанных в научных журналах!
Сейбел: Давайте сменим тему. Можете ли вы припомнить худшую из ошибок, которую вам довелось отлавливать?
Айк: Худшие ошибки связаны с многопоточностью. В Silicon Graphics я делал работу, связанную с ядром UNIX. Как и все тогдашние UNIX-ядра, оно представляло собой монолитный монитор, который завершался после вхождения в ядро через системный вызов. Исключая прерывания, оно гарантированно работало вплоть до завершения, и блокировка вашей структуры данных никогда не наступала. Прекрасно и просто.
Но вот в SGI пришли блестящие молодые умы из HP. Настала эпоха симметричной мультипроцессорной обработки данных. Старая группа, которая занималась ядром, распалась. Теперь ядро делали новые ребята. Темп работы сильно ускорился, но какие у них были инструменты? Си, семафоры, блокировки, возможно, также мониторы, условные переменные. Все коды написаны от руки. Тысячи ошибок. Полный кошмар.
Мне предложили тогда съездить в Австралию и Новую Зеландию - я описал все это в своем блоге. Мы тогда как раз исправляли ошибку в полевых условиях. Это было страшно тяжело - найти ее и исправить, потому что ошибка была такого свойства: код для однопроцессорного ядра помещался в ядро, созданное для симметричной мультипроцессорной обработки данных, и мы совсем не беспокоились насчет определенных условий гонки. Поэтому для исправления пришлось создавать контрольный пример, что само по себе было непросто. И все это при нехватке времени - клиент хотел исправления в полевых условиях.
Диагностировать ее было трудно, так как она была связана с синхронизацией по времени. Машины использовались не по назначению, как концентраторы терминалов. Люди подвешивали псевдотерминалы к реальным терминалам. Это делали студенты в лаборатории или сотрудники брисбенской компании, производившей ПО для горной промышленности: множество отсеков и в конце стеклянная стена, а за ней компьютеры, в том числе двухпроцессорная машина от SGI. Было нелегко, и я рад, что мы все же нашли ошибку.
Обычно такие ошибки не сидят годами, но отыскать их крайне трудно. Нужно как бы приостановить все, думать о них постоянно, видеть их во сне... А заканчивается все тем, что вы делаете элементарные вещи. Так бывает со многими ошибками. Все заканчивается бисекцией, по методу волка и забора. Вы постоянно следите за выполнением, за состоянием памяти, пытаетесь прикинуть размер ошибки, течение исполнения программы, понять, к каким данным можно обратиться. Если это куча голых указателей, дело плохо: следует обратиться к более современным инструментам, которые появились вместе с гигагерцными процессорами, вроде Valgrind и Purify.
Инструментирование и наличие контролируемой модели всей иерархии памяти - это большое дело. Роберт О'Каллагэн, могучий новозеландский ум, создал собственный отладчик на базе Valgrind: он записывает каждую инструкцию, и можно в любой момент восстановить состояние программы целиком. Это не только отладчик, путешествующий во времени. Это целая база данных: вы видите структуру данных, замечаете поле с безумными значениями, выясняете, кто делал там последнюю запись. Вы идете от следствий к причинам - в отладке это занимает очень много времени. Это в тысячу раз медленнее, чем все происходит в реальном времени, но у вас есть надежда.
Можно также использовать записывающие виртуальные машины - они записывают состояние только при системных вызовах и на границах ввода/вывода. Они могут воссоздать состояние поврежденной программы на каждой границе - правда, со всем, что между границами, намного сложнее. Зато все можно закончить быстро, практически в реальном времени, потом перенести программу в Chronomancer, запустить ее в медленном темпе, воссоздать все состояния и найти ошибку.
К сожалению, технология отладки мало исследована. Вот еще пример пропасти между учеными и практиками. Ученые создают доказательства правильности, часто вручную, - правда, эта работа все больше автоматизируется благодаря POPLmark и подобным инструментам. Но в реальной жизни везде есть только отладчики, встречаются даже развалюхи родом из 1970-х, вроде GDB.
Сейбел: В реальной жизни есть еще разрыв между приверженцами символических отладчиков и операторов вывода?
Айк: Да. Поэтому я использую GDB и доволен тем, что в нем, по крайней мере на Маке, есть возможность поставить точку прерывания, и это обычно работает. Я могу наблюдать за адресом, могу засечь момент, когда правильные биты сменяются неправильными. Это довольно полезно. Я также использую команду printf для бисекции. Когда я уже близок к цели, то обычно пытаюсь сделать что-то внутри GDB или пользуюсь командными сценариями (скриптами), хотя они очень слабы. Язык сценариев сам по себе очень слаб. По-моему, Ван Якобсон добавил циклы, но не знаю, использовались ли они в настоящем GDB, после семинаров, организованных Фондом свободного программного обеспечения.
Однако отладчики могут сделать бесконечно больше, чем делают сейчас, и в этом смысле Chronomancer и Replay - шаг вперед. Они изменили для меня весь процесс. Но вот насчет многопоточности не знаю. Есть Helgrind и другие динамические детекторы гонок, которыми мы пользуемся. Они дают ложные срабатывания, которые надо отсеивать. С ними пока еще не все до конца понятно.
Многопоточные процессы страшат меня - до того как я женился и завел детей, они съели немалую часть моей жизни. Не все задумываются насчет параллелизма и всех возможных комбинаций команд, происходящих даже в небольших сценариях. Если ваш код соединяется с чужим, он выходит из-под контроля. Вы не можете мысленно смоделировать происходящее. Большинство людей не могут. Я мог бы стать одним из этих всезнаек с сайта Slashdot: когда я писал в своем блоге, что против многопоточности, кое-кто говорил: "Да он ничего не знает, это несерьезно". Давай, болтай. Я слетал в Австралию и Новую Зеландию, мне достались кое-какие бонусы. Но все это было так мучительно и продолжалось слишком долго. Как сказал Уайльд о социализме, "Он отнимает слишком много вечеров".
Сейбел: Как вы проектируете код?
Айк: Я много прототипирую. Создаю нечто вроде высокоуровневого псевдокода и затем постепенно заполняю его снизу вверх. Этот псевдокод я обычно не пишу, а держу в голове, и иду снизу вверх, пока оба конца не сойдутся. Часто я работаю с готовыми фрагментами кода, добавляя новую подсистему или что-то постороннее, и почти все делаю снизу вверх. Если в середине я сталкиваюсь с трудностями, то опять пишу псевдокод и начинаю работать все в том же направлении - снизу вверх, пока не закончу его. Я стараюсь не затягивать с этим, так как проверить псевдокод невозможно - надо смотреть, как он работает, выполнять его шаг за шагом, убеждаясь, что он делает именно то, чего от него ждешь.
Еще до этого этапа я могу установить кое-какие связи между объектами, вчерне набросать модули. Обычно есть два-три алгоритма, и можно прикинуть их сложность - линейная она или константная. Всякий раз, когда я выполнял поиск с линейной сложностью, который затем складывался в квадратичный, для веб-разработчиков это была проблема. Поэтому мы предпочитаем делать много структур данных с константным временем доступа. Но даже и тогда эта константа может не быть единицей - она может быть достаточно большой, чтобы о ней побеспокоиться.
Поэтому мы создаем множество прототипов, работаем над разными кусками с обоих концов, которые сводим в середине. Я считаю, что сейчас мы в Mozilla недостаточно переписываем код. Мы слишком консервативны. У нас открытый исходный код, поэтому вокруг нас создаются сообщества, мы заинтересованы в новых людях. Мы работаем в интересах пользователей и поэтому не можем позволить себе трехлетний перерыв на переписывание - хотя если очень постараться, то смогли бы.
Но если вам нужно избрать другой путь, и вы не знаете в точности какой, - переписывайте. Если хотите понять, что, черт возьми, вы тут делаете, потребуется несколько попыток. Код становится лучше по своему проектному решению, вы останавливаетесь на этой версии, начинаете ее латать, пока не дойдете до предела. Это нечто вроде эволюционного тупика для кода. Возможно, при приемлемых невозместимых издержках этот код останется на годы. А может, потребует замены. Вдруг в мире открытого исходного кода появится более совершенная стандартная библиотека?
Это возвращает нас к ремеслу программирования. Вы не просто пишете код в соответствии со старым проектным решением. Вы хотите постоянно практиковаться, а это значит думать о проектном решении кода и применять свой опыт в написании кода к этому решению.
У меня страшная аллергия на всякого рода эзотерические решения, шаблоны проектирования, доступные немногим. Питер Норвиг, работая в Harlequin, сказал о том, что шаблоны проектирования - всего лишь дефекты в вашем языке. Возьмите язык получше! И он был абсолютно прав. Что это такое - молиться на шаблоны, постоянно думая, какой бы из них применить!
Сейбел: Итак, постоянное обогащение опыта позволяет лучше выбирать направление. Но что если, создавая код, вы видите крупные дефекты в проектном решении?
Айк: Так бывает и нередко. Иногда очень сложно отказаться от кода и вернуться, чтобы начать все заново, - попадаешь в ловушку обязательств. У меня было такое с JavaScript. Я написал интерпретатор байт-кода в страшной спешке, и делая это, я уже понимал, что кое о чем пожалею. Но то было решение, понятное для других, и я надеялся, что другие помогут мне с этой программой. Поэтому о проектном решении я думаю всегда - не каждый раз мы можем позволить себе роскошь пересматривать фундаментальные основы кода. А именно это случается при масштабном переписывании.
Сейбел: Каким образом вы решаете, что необходимо масштабное переписывание? Благодаря Джоэлу Спольски Netscape в некотором смысле стала примером того, как опасно масштабное переписывание.
Айк: В Netscape хотели, чтобы приобретенная ими компания, потрясавшая известной книгой о шаблонах проектирования, вывела их на первое место благодаря новому движку рендеринга, который стал бы для всех ориентиром. Сверху это смотрелось хорошо: там использовались C++ и шаблоны проектирования. Но было множество проблем.
А вот вторая причина того, что мы взялись за переписывание: я трудился в mozilla.org и был сильно недоволен Netscape, как и Джейми, - тот вообще собирался уходить. Я считал, что нужно пустить на наше поле новых работников, а со старым запутанным кодом, сделанным на коленке в 1994 году, сделать это было нельзя. И с моим прекрасным кодом интерпретатора в стиле ядра UNIX.
Нам нужна была большая перезагрузка. Четыре года с момента выпуска программы! Не говоря ничего такого топ-менеджерам, поскольку они и слушать об этом не желали, мы стали искать оптимальное решение за них. В итоге полетело несколько топ-менеджерских голов. Правда, менеджеры, в отличие от меня, все равно сказочно заработали на опционах. Но для Mozilla это было выгодно.
Сегодня можно назвать это удачей, поскольку развитие Сети ускорилось. Видимо, Microsoft - некоторые утверждают, что это было связано скорее с антимонополистскими расследованиями, чем с желанием его руководителей, - хотела плотно оседлать Сеть и не допустить ее эволюции. Это дало нам возможность заняться переписыванием, размахивая знаменем стандартов (довольно сомнительный ход, особенно с учетом качества стандартов). Как и Джоэл, я скептично смотрю на переписывание. Трудно примирить все интересы, найти деньги и при этом правильно отреагировать на требования рынка. Исключений единицы.
Те случаи переписывания, о которых я говорил раньше, связаны с прототипами. Это крайне важно, а объем работы намного меньше. Можно порезать кучу кода, не очень много по числу строк, но с большими последствиями, и удовлетворить всем нужным инвариантам. Или это компиляция "на лету", или еще что-то, что позволяет решить задачу.
Сейбел: Вы занимались литературным программированием в духе Кнута?
Айк: Я делал кое-что наподобие его первоначальных программ - просто классно, мне очень нравилось. Это было извлечение слов. Там было некое подобие древовидного хеша, программирование было целиком литературным. Потом Дуг Макилрой сделал все то же самое, только с конвейером.
Наши программы подробно откомментированы, но нет средства изъять из них прозу и проверить ее - хотя бы вручную - относительно кода. Разработчики Python сделали в этом смысле кое-что интересное. Все что я сделал - не более чем подробное комметирование. Я периодически обновляю старые комментарии, но это тяжело, и иногда мне это не удается, и я жалею, что кто-то из-за меня получил неверную информацию.
Сейчас мне нравятся возражения Макилроя. Это не то что полное опровержение литературного программирования, но близко к тому. Не хочется писать много - неважно, прозы или кода. В каком-то смысле, на низшем уровне код должен говорить сам за себя. На более высоких уровнях - гигантские функции, границы модулей - уже нужна документация. Документирующие комментарии, или документарные строки. Встраивание тестов в комментарии. Думаю, разработчики Python сделали тем самым большое дело.
Кое-что, как видно, пришло из литературного программирования - документарные строки, встроенные тесты. Хотелось бы, чтобы языки поддерживали больше таких вещей. Мы пытались включить встроенную документацию в ES4 через поддержку метаданных первого класса или интроспекции, но не смогли договориться между собой.
Сейбел: Вы читаете чужой код?
Айк: Это часть моей работы. Ревизия кода - обязательный шаг перед коммитом, который был когда-то необходим в основном из-за плохой кадровой политики Netscape, но мы до сих пор пользуемся им, а также делаем интеграционные ревизии. Мы устраиваем также особые "суперревизии", когда изменяется много модулей и вы не знаете всех скрытых инвариантов, которые Джо Шмо, который больше не работает в Mozilla, держал в своей голове. В принципе, есть люди, способные охватить взглядом целостную картину. Иногда мы обходимся без этого, когда все хорошо знают, что делают, и понимают друг друга без слов, как джедаи. Но лучше поступать так не слишком часто.
У нас нет предварительных ревизий проектных решений. Поэтому иногда такие ревизии случаются потом сами. "У тебя слишком много кода. Вернись-ка назад и сделай по-другому". Но так бывает редко. Мы не навязываем "модель водопада", жестко последовательную схему разработки. Когда я занялся программированием в начале 1980-х, она была как раз в моде, просто кошмар. Вы пишете документацию, потом код, потом понимаете, что код не годится, вы его полностью меняете - и вся документация насмарку.
Сейбел: Вы говорите о коде, который пишется для Mozilla. А вы читали когда-нибудь код не по работе, а ради самообразования?
Айк: В этом смысле открытый исходный код - потрясающая вещь. Я люблю смотреть код, который пишут люди с разных концов света, хотя посвящаю этому не так много времени. Больше всего меня привлекают серверные фреймворки, а еще такие языки, как Python и Ruby.
Сейбел: То есть их реализации?
Айк: Реализации и код библиотек. Возьмем библиотеки Ajax: приятно видеть, насколько умны могут быть люди и как с небольшим набором инструментов - замыкания, прототипы, объекты - можно создавать удобные, порой очень удобные абстракции. Нельзя сказать, что они всегда крепко скроены или безопасны, - но до чего удобны!
Сейбел: Как вы поступаете, если надо прочесть большой кусок кода?
Айк: Я начинаю "сверху". Если фрагмент достаточно велик, у вас есть указатели функций, а течение исполнения программы не слишком ясно. Иногда я пропускаю его через отладчик и терзаю таким вот способом. Я также смотрю на шаблоны низкого уровня, которые могу распознать. Если это языковой процессор или в нем есть понятные мне системные вызовы, я пытаюсь выяснить, как используются эти примитивы. Как они используются на более высоких уровнях? Это позволяет мне немного освоиться с кодом. Но по-настоящему его понимаешь, когда создается целостный образ, когда смотришь на код сверху, снизу, с разных сторон, возясь с отладчиком, пропуская через него код шаг за шагом, как бы это ни раздражало.
Если удается посмотреть, что происходит в куче - проследить указатели, пройтись по списочным ячейкам, что-то еще, - осознаешь, что это стоило труда, как бы тошно ни казалось. Для меня это так же важно, как чтение исходника. Исходник можно читать долго, можно застрять в нем, смертельно скучая и понапрасну уверяя себя, что понимаешь вон то темное место.
Создавая регулярные выражения в JavaScript, я обращался к Perl 4. Я пропускал его через отладчик и читал код. Это давало мне идеи: реализация в обоих языках оказалась схожей. Их рекурсивный характер с поиском с возвратом был немного необычен, так что пришлось поднапрячься. Удалось только отладить некоторые регулярные выражения и проследить за исполнением. Другие программисты, как я слышал, тоже говорили об этом: нелегко пробираться сквозь код, понимать, как динамическое состояние системы выглядит при беглом взгляде или при проверке базовой функциональности. Я согласен с ними.
Сейбел: Вы делаете то же самое с собственным кодом, даже если не ищете ошибку?
Айк: Разумеется, я делаю проверку на вменяемость. И делаю много операторов-утверждений, и если они срабатывают, я тут же оказываюсь в отладчике. Но иногда пишешь код, применяя разные хитрые вспомогательные схемы. Тестируешь его, он работает... пока не пропустишь его через отладчик. Особенно если в нем есть особо заумный кусок, срабатывающий, только если звезды сходятся определенным образом. Используешь условные точки прерывания, точки прерывания по изменению данных, наконец, ловишь его, когда этот код срабатывает, - ага, звезды сошлись! И понимаешь, что живешь не на планете сказок. Если, пребывая в исходном коде, вы чувствуете себя обитателем этой планеты, беритесь за отладчик. Это важно, я так всегда поступаю.
Сейбел: Вы обнаруживаете проблему, когда пробираетесь сквозь исходник, держа в уме, что должно произойти вот это и вот это, но ничего не происходит?