Обратимся к простой задаче программирования, называемой задачей вычисления среднего:
Составить программу чтения последовательности положительных чисел, которая останавливается, если прочитано число 99999. Вычислить среднее арифметическое из этих чисел, не включая в вычисление среднего число 99999. Обеспечить отбрасывание любого входного значения, не являющегося положительным числом.
Программа, написанная студентами, должна вычислять среднее для последовательности положительных чисел. В ней необходимо обеспечить, чтобы входная величина при этом была положительной. Ввод данных прекращается, когда прочитано специально выделенное значение: 99999. Подобные величины, являющиеся сигналом конца ввода, называются сигнальными значениями.
На рис. 1,а приведен пример решения задачи вычисления среднего. Программа работает, но дает следующую ошибку: если вы набрали на терминале 99999 сразу же после какого-либо неположительного числа, то программа будет запрашивать новые данные, несмотря на то что прочитано число 99999. В результате после завершения работы программы вычисленное среднее будет неверным. Например, если вы ввели 5, -5, 99999, то вместо окончания работы программа выдает запрос на новые входные данные. Если затем ввести еще раз 99999, то программа напечатает не 5, а (5+99999)/2, или 50002.
Рис. 1. Пример одной из попыток новичка написать программу вычисления среднего арифметического (а). Система PROUST объясняет ошибки, вкравшиеся в программу, и даже предлагает те данные, на которых эта ошибка проявляется (б).
Когда поступает последовательность 5, -5, 99999, программа интерпретирует 99999 как данные, поскольку, когда программа читает —5, она входит в цикл проверки допустимости входных величин, который начинается строкой 10, WHILE Val<=0DO. Этот цикл устроен так, что он повторяется до тех пор, пока не будет напечатана положительная величина. Так как число 99999 положительно, при его чтении работа цикла заканчивается и происходит передача управления. Однако программа написана в предположении, что, когда происходит выход из цикла проверки допустимости входной величины, текущее значение Val обязательно является допустимым входным значением. В нашем случае Val не является допустимым значением — это 99999, т. е. сигнальное значение. В цикле же эта величина обрабатывается, как если бы она была допустимой. Чтобы предусмотреть возможность подобной ошибки, после цикла проверки допустимости входной величины необходимо ввести проверку на сигнальное значение.
На рис. 1,б показано сообщение системы PROUST, описывающей пропущенную проверку на сигнальное значение. Эта ошибка представлена двояко. Во-первых, дается ее описание на естественном (в оригинале на английском.— Перев.) языке, а во-вторых, система PROUST строит пример сочетания данных, на котором программа срабатывает неверно.
Теперь обратимся к программе, показанной на рис. 2,а. Это другое решение задачи вычисления среднего, в котором ошибка достаточно глубоко скрыта. Если на терминале набрать положительное число, а за ним — отрицательное, то последнее будет учтено при вычислении среднего. Так, если вы напечатаете —2, 2, 99999, то среднее будет равно 2, но если вы напечатаете 2, —2, 99999, то среднее будет равно 0.
Рис. 2. Другая попытка новичка составить программу вычисления среднего арифметического (а). Система PROUST вновь объясняет, в чем состоит затруднение с программой, а также показывает, что программист хотел сделать и что он сделал на самом деле (б).
В отличие от примера, приведенного на рис. Здесь 1,а здесь программист не забыл о проверке, связанной с сигнальным значением, но записал ее с использованием команды WHILE вместо команды IF. По-видимому, у студента сложилось неправильное представление о том, чем различаются эти два оператора, и он не понимает, как происходит передача управления в цикле, содержащем WHILE. Пока такой цикл остается прямолинейной цепочкой команд, студент не сталкивается ни с какими трудностями. Однако если в цепочку вклиниваются другие проверки, то студент считает, что их также следует записывать с использованием оператора WHILE, видимо, чтобы гарантировать их повторение, когда происходит повторение всего цикла. Впредь мы будем называть такого сорта недоразумение «путаницей WHILE с IF». На рис. 2,б показана реакция системы PROUST, которая с учетом случившегося недоразумения дала студенту соответствующее объяснение.
Ошибки, приведенные на рис. 1,а и 2,а, иллюстрируют следующее. Во-первых, ошибки часто невозможно обнаружить, если не знать заранее, чтб должна была бы делать рассматриваемая программа. Обе программы идут независимо от того, что поступает им на вход, и чтобы обнаружить ошибку, надо убедиться в том, что выдаваемые программами результаты отличаются от тех, которые должны быть на самом деле. Ошибки такого типа встречаются нередко: проверка на сигнальное значение оказывается пропущенной в 18% программ вычисления среднего, составленных начинающими программистами.
Во-вторых, студенты не в состоянии обнаружить подобные ошибки без подсказки, поскольку программа дает неверные результаты при необычных сочетаниях входных значений, и маловероятно, чтобы начинающий программист догадался их попробовать. В случае же «путаницы WHILE с IF», даже если программист и попробует такие входные значения, он, по всей видимости, не сможет понять, почему программа сбивается, — ведь он ожидает от оператора WHILE не того действия, которое тот на самом деле производит.