Обычно выбор имен переменных и организация кода - забота скорее о читателе, о форматировании, которое безразлично компьютеру, но облегчает чтение человеку.
Сейбел: По мере того как языки улучшаются или хотя бы становятся дружественными к программисту - в сравнении с временами ассемблера и перфокарт, - избегать ошибок в программах вроде бы становится легче. Есть компиляторы, сигнализирующие об ошибках, и так далее. Так можно ли отдать предпочтение - пусть небольшое - читаемости кода перед правильностью? Как говорят разработчики на Haskell: “Если ваша программа на Haskell выполняет проверку типов, можно спать спокойно”.
Стил: Думаю, это страшное заблуждение. В пропущенных через компилятор программах всегда слишком много ошибок, чтобы спать спокойно. А если есть ошибки, они собьют с толку не только компьютер, но и человека, читающего код.
Программирование - глубоко неестественный вид деятельности, и ему надо как следует учиться. Люди привыкли, что их слушатели сами восстанавливают лакуны в речи. И с компиляторами мы обращаемся в какой-то мере так же. Говоря: “Мне нужна переменная с именем f оо”, - вы не заботитесь о регистре имени. Люди неточны, часто неряшливы в своей речи. Но когда мы даем команды машине, детали важны, потому что мелкая погрешность может изменить ход всего процесса.
Мне кажется, люди привыкли к использованию рекурсии в ограниченном виде - кажется, это показал Ноам Хомский. Но на деле люди редко углубляются больше чем на три уровня, а если и углубляются, то обычно в стиле хвостовой рекурсии. Понимание рекурсии есть сложнейшее искусство. А ведь рекурсия - один из самых мощных инструментов программиста, если его как следует освоить. И поэтому заботиться о корректности надо всегда.
Сейбел: Тем не менее многие пытались разработать языки или системы, дающие возможность написать программу непрограммисту. Считаете ли вы эти попытки обреченными на неудачу - ведь дело не в правильности синтаксиса, а в том, что программирование неестественно по природе.
Стил: Да. И, кроме того, люди сосредоточены на главном, они не думают о пограничных случаях, о сложных случаях, о маловероятных случаях. А именно в таких случаях начинаются разногласия - как сделать правильно.
Иногда я спрашиваю студента: “Что будет в таком-то случае?” - “То-то и то-то”. И тут же кто-нибудь вскакивает: “Нет, должно быть вот так!” Вот такие вещи и надо отражать в программной спецификации.
Неслучайно для описания процесса программирования мы пользуемся терминами из арсенала магии. Мы говорим: что-то произошло как по волшебству или автоматически. Думаю, это оттого, что заставить машину сделать нужную тебе вещь - почти то же самое, что добиться исполнения желания.
Посмотрите: героям волшебных сказок достаточно придумать желание, махнуть рукой - и вот оно выполнено. И, конечно, сказки полны поучительных ситуаций: герой забыл учесть пограничный случай - и из-за этого случилось что-то нехорошее.
Сейбел: Возьмем “Fantasia” - она в том числе об опасности рекурсии.
Стил: “Fantasia” и рекурсия, да. Или “Я хочу быть самым богатым человеком в стране”, - в итоге все становятся бедняками, а он остается при своем. Такое в волшебных сказках случается, потому что люди забывают о разных путях, ведущих к цели. Если думать только о главном желании, пренебрегая деталями, много чего не стыкуется.
Сейбел: Каков же урок волшебных сказок? Гэндальфы становятся великими магами путем тяжких трудов и зубрежки заклинаний, а легкого пути нет?
Стил: Да. Другой пример. Допустим, я говорю своему умному компьютеру: “Хочу, чтобы имена в моей телефонной книге шли по алфавиту”, - и он выбрасывает все имена, кроме первого. Алфавитный порядок не нарушен, но это не то, чего мне хотелось. И оказывается, что спецификацию вида “хочу упорядочить имена по алфавиту, без потери данных, без дублирования” чертовски трудно написать.
Сейбел: Получается, есть свойства языков, которые делают труд программистов, освоивших эту неестественную работу, более продуктивным? Вы сейчас проектируете язык, у вас должно быть мнение на этот счет.
Стил: Я уже говорил: нельзя пренебрегать точностью. В то же время мы можем создавать инструменты для повышения точности. Мы не можем сделать эту процедуру тривиальной, но можем помочь избежать некоторых ошибок. Вместо того чтобы допускать циклическое переполнение 32- битных целых, можно сделать обнаружение арифметического переполнения или предоставить возможность работы с большими числами. Сейчас реализация всего этого обходится дороже, но я считаю, что работа с настоящими большими числами дает немного меньше ошибок для некоторых видов программирования. Программисты и создатели алгоритмов для операционных систем все время попадают в одну и ту же ловушку. Они говорят: “Нам нужно синхронизировать фазы, так что будем брать по одному числу. При начале новой фазы вычисления будем увеличивать на единицу какую-нибудь переменную, получать новое число - и тогда все участники будут уверены, что работают в одной и той же фазе, пока не начнется какая- нибудь операция”. Это работает на практике, но с 32-битными числами вы досчитаете до 4 миллиардов довольно быстро. Что будет в случае циклического переполнения? Все будет в порядке? Или нет? Многие подобные алгоритмы в компьютерной литературе содержат эту небольшую ошибку. Что если какой-нибудь поток застопорится со 2-й по 32-ю итерацию? Маловероятно, но все-таки возможно. Надо или как-то смягчить эту проблему в смысле корректности, или все просчитать и показать, что такой вариант настолько маловероятен, что беспокоиться не о чем. Или, возможно, для вас приемлем один компьютерный сбой в день. Суть в том, что надо проанализировать проблему, а не игнорировать ее. Тот факт, что счетчик может переполниться, - проблема еле заметная, большинство программистов она не затронет. Но для остальных это ловушка в их алгоритмах.
Сейбел: Кстати о сбоях. Какова худшая ошибка, с которой вы имели дело?
Стил: Не уверен, что назову худшую, но могу кое-что рассказать. Самые трудноуловимые ошибки порождаются параллельными процессами.
Когда я был зеленым программистом и работал на IBM 1130, решение, как исправить ошибку, однажды явилось мне во сне. Или сразу после пробуждения. Я бился над ней пару дней, ничего не получалось. И вот посреди ночи - озарение. Оказалось, я кое-что пропустил в спецификации интерфейса.
Это было связано с параллельными процессами. Я писал декомпилятор, чтобы декомпилировать и изучить дисковую оперативную систему машин IBM. Для этого надо было взять с диска данные в двоичном виде и распечатать их в разных форматах - как инструкции, как коды символов, как числа и так далее. Для преобразования символов я скармливал их разным функциям преобразования, одна из которых была предназначена для работы с кодом, считанным через устройство для чтения перфокарт. И я пропустил крохотное примечание в спецификации: “Прежде чем вызвать эту функцию, необходимо очистить младшие биты в буфере, в который будут считываться данные с перфокарты”. Или, наоборот, их надо было установить.
Так или иначе, 12 бит с карты записывались в старшие 12 бит 16-битного слова, а младшие разряды использовались для хитрого трюка: можно было запустить функцию чтения перфокарты асинхронно, и тогда буфер заполнялся тоже асинхронно, и при этом выполнялась функция преобразования. И этот младший разряд определял, была ли считана следующая колонка перфокарты. Если была, то выполнялось преобразование. Таким образом, почти сразу после считывания всей перфокарты преобразование завершалось - за счет того, что эти процессы перекрывали друг друга, получался выигрыш во времени. Я же скармливал в функцию сырые двоичные данные, которые не подчинялись этим ограничениям. Я просто не обратил внимания на примечание. Я думал, что это обычная функция преобразования, а оказалось, что в интерфейсе этой функции есть особенность: она задействовала младшие разряды, о которых обычно думать не приходится. Она обрабатывала буфер и говорила мне: “Данные еще не поступили из устройства для чтения перфокарт”. В принципе, я знал, что такое возможно, но тогда это мне в голову не пришло. А потом во сне меня озарило. Вот такой странный случай.
А вот другая занятная история. Я отвечал за Maclisp, a Maclisp поддерживал большие числа - целые числа произвольной точности. Они у нас были уже несколько лет, считалось, что они хорошо отлажены.