Курилка

Почему Паскаль — не мой любимый язык программирования

Brian W. Kernighan, 2 апреля 1981 года
AT&T Bell Laboratories, Murray Hill, New Jersey 07974

Введение

Язык программирования Паскаль стал доминирующим в обучении компьютерным наукам. Он также оказал сильное влияние на более поздние языки, в частности Ада.

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

Паскаль, по крайней мере в его стандартной форме, просто не пригоден для серьёзного программирования. Эта статья отражает моё собственное исследование некоторых причин, почему это так.

1. Начало

Эта статья берёт начало от двух событий — серии статей о сравнении Си и Паскаля (1, 2, 3, 4) и моих собственных попыток переписать «Software Tools» (5) на Паскале.

Сравнивать Си и Паскаль — всё равно, что сравнивать самолеты Learjet и Piper Cub — один создан для дела, а другой для обучения; поэтому такое сравнение может показаться надуманным. Но переработка «Software Tools» — уместное сравнение. Программы в этой книге были изначально написаны на Ратфоре (Ratfor) — «структурном» диалекте Фортрана, реализованном препроцессором. Т.к. Ратфор в действительности — лишь замаскированный Фортран, он имеет некоторые свойства, общие с Паскалем: типы данных, более подходящие для работы с символами, средства структурирования данных для лучшего описания их организации и строгая типизация для манипулирования заведомо известными структурами.

Переписать программы на Паскале оказалось труднее, чем я мог себе представить. Эта статья является попыткой извлечь из опыта некоторые уроки о пригодности Паскаля для программирования (а не для обучения программированию). Она не является сравнением Паскаля с Си или Ратфором.

Сначала программы были написаны на диалекте Паскаля, поддерживаемом интерпретатором под названием pi, предоставленным Университетом Калифорнии в Беркли. Язык близок к номинальному стандарту Йенсена и Вирта (6), с хорошей диагностикой и тщательной проверкой в процессе выполнения. С тех пор программы также запускались (без изменений, за исключением новых библиотек примитивов) на четырех других системах: интерпретаторе от Амстердамского свободного университета (далее VU, от Vrije Universiteit), VAX версии системы Беркли (компилятор), компиляторе, поставляемом Whitesmiths Ltd. и UCSD Pascal на Z80. Все они написаны на Си, за исключением последнего.

Паскаль — очень обсуждаемый язык. Новейшая библиография (7) перечисляет 175 статей под заголовком «обсуждения, анализ и дискуссии». Наиболее цитируемые источники (их стоит прочесть) — это жёсткая критика от Хаберманна (8) и не менее твердое возражение в ответ от Lecarme и Desjardins (9). Статья за авторством Boom и DeJong (10) также заслуживает внимания. Оценка Паскаля самим Виртом есть в [11]. У меня нет ни желания, ни возможности резюмировать литературу; эта статья представляет мои личные наблюдения, и большинство из них неизбежно повторяет те же моменты, на которые указывают другие авторы. Я попытался выстроить остальной материал вокруг следующих проблем:

А в пределах каждой части — более-менее в порядке убывания важности.

Сформулирую мои выводы с начала: может быть, Паскаль является превосходным языком для обучения новичков программированию; у меня лично такого опыта нет. Это было значительное достижение для 1968 года. Паскаль, безусловно, повлиял на структуру современных языков, из которых, возможно, Ада является наиболее важным. Но в своей стандартной форме (как в текущей, так и предлагаемой) Паскаль недостаточен для написания реальных программ. Он пригоден только для малых, самодостаточных программ, которые незначительно взаимодействуют со своим окружением и не используют никакое программное обеспечение, написанное кем-либо еще.

2. Типы данных и области видимости

Паскаль является языком со строгой (почти) типизацией. Грубо говоря, это означает, что каждый объект в программе имеет четко определенный тип, который всецело определяет допустимые значения и операции над объектом. Язык гарантирует, что он запрещает недопустимые значения и операции некоторой смесью проверок во время компиляции и выполнения. Конечно же, компиляторы могут не выполнять все проверки, сформулированные в языке. Более того, не следует путать строгую типизацию с теорией подобия. Если кто-либо определит типы «яблоко» и «апельсин» как

type
  яблоко = integer;
  апельсин = integer;

То любые арифметические выражения с участием яблок и апельсинов будут вполне допустимы.

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

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

Давайте пройдемся по некоторым проблемам типов и областей видимости.

2.1. Размер массива — часть его типа

Если объявить

var
  arr10 : array [1..10] of integer;
  arr20 : array [1..20] of integer;

Тогда arr10 и arr20 будут массивами 10-ти и 20-ти целых чисел соответственно. Предположим, что мы хотим написать процедуру «sort» для сортировки целочисленного массива. Поскольку arr10 и arr20 имеют разные типы, невозможно написать одну процедуру, которая сортировала бы их оба.

Влияние этого на «Software Tools» в частности, и, я думаю, на программы вообще, состоит в том, что становится реально трудно создавать библиотеки процедур для выполнения общих, универсальных операций вроде сортировки.

Особый тип, наиболее часто подверженный этому, это «array of char», представляющий в Паскале строку, поскольку это массив символов. Предположим, что мы пишем функцию «index(s,c)», которая возвращает первую позицию символа «c» в строке «s», либо ноль, если он не встречается. Проблема заключается в обработке строкового аргумента функции «index». Вызовы «index('привет', c)» и «index('пока', c)» не могут быть допустимы оба, поскольку строки имеют разные длины. (Я пропускаю вопрос определения конца константной строки вроде «привет», поскольку это невозможно). Следующая попытка:

var temp : array [1..10] of char;

temp := 'привет';
n := index(temp, c);

Но это присвоение переменной temp недопустимо, поскольку «привет» и «temp» имеют разные длины.

Единственный выход из этого бесконечного поиска истоков заключается в определении семейства процедур с членами для каждой возможной длины строки. Либо сделать все строки одной длины (включая константные вроде «определение»).

Последний подход — меньшее из двух больших зол. В «Tools» тип, названный «string», определен как:

type string = array [1..MAXSTR] of char;

Где константа «MAXSTR» выбрана как «достаточно большая», и все строки во всех программах имеют в точности этот размер. Это далеко от идеала, хотя и позволяет добиться работающих программ. Это не решает проблему создания настоящих библиотек с полезными процедурами.

В некоторых случаях массивы фиксированного размера неприменимы. Например, в «Tools» программа сортировки строк текста работает с помощью заполнения памяти как можно большим количеством строк, которое она может вместить. Время ее выполнения жестко зависит от того, как может быть упакована заполненная память.

Таким образом, для «sort» используется другое представление — большой массив символов и набор индексов в этом массиве:

type
  charbuf = array [1..MAXBUF] of char;
  charindex = array [1..MAXINDEX] of 0..MAXBUF;

Но процедуры и функции, написанные для обработки представления фиксированной длины, не могут использоваться с данными переменной длины. Полностью новый набор процедур необходим для копирования и сравнения строк в таком представлении. В Fortran или C одни и те же функции могут использоваться для обоих форм.

Как указано выше, константная строка, записанная как

'это строка'

имеет тип «packed array [1..n] of char», где n — длина строки. Таким образом, каждый строковый литерал различной длины имеет разный тип. Единственный способ написать процедуру, которая выведет сообщение и закончит работу, это расширить сообщения до одной максимальной длины:

error('короткое сообщение '); (* 18 символов + 10 пробелов в конце строки = 28 символов *)
error('сообщение несколько подлиннее'); (* 28 символов *)

Множество коммерческих компиляторов Паскаля предоставляют тип данных «string» (строка), который обходит это ограничение явно: все такие строки имеют один тип, вне зависимости от их длины. Это решает проблему для одного типа данных, но не больше. Также это не решает второстепенные проблемы, такие как подсчет длины константной строки, обычное решение — другая встроенная функция.

Энтузиасты Паскаля заявляют, что побороть проблему размера массивов можно простым копированием нескольких библиотечных функций и похоже заполнить параметры для программы. Но эта оборона, в лучшем случае, звучит слабо: (12)

«Так как границы массива — часть его типа (а точнее, типа его индексов), невозможно определить процедуру или функцию, которая применима к массивам с различными границами. Хотя это ограничение может показаться строгим, по опыту разработки на Паскале мы склоняемся к тому, что оно встречается очень редко. [...] Тем не менее, в связи с использованием программных библиотек, необходимость привязки к размеру параметрических массивов является серьезным дефектом.»

Эта небрежная работа — самая большая из проблем Паскаля. Я верю, что это может быть исправлено, и язык станет значительно более практичен. Предложенный ISO стандарт Паскаля (13) предлагает некоторое решение («совместимые схемы массивов»), но одобрение этой части стандарта, очевидно, все еще под вопросом.

2.2. Нет статических переменных и инициализации

«Статическая» переменная (часто также называемая «собственной» приверженцами Алгола), это такая переменная, которая принадлежит какой-либо процедуре и сохраняет свое значение между вызовами этой процедуры. В Фортране переменные внутренне статичны де-факто, за исключением COMMON. В Си есть объявление «static», которое может быть применено к локальным переменным. (Строго говоря, в Fortran 77 необходимо использовать SAVE для включения статического атрибута.)

В Паскале нет подобного класса памяти. Это означает, что если функция или процедура Паскаля планирует помнить значение между вызовами, то переменная должна быть внешней по отношению к функции/процедуре. Соответственно, она должна быть видима другим процедурам и её имя должно быть уникально в большей области. Простой пример — генератор случайных чисел: значение, использованное для получения текущего возвращаемого значения, должно быть сохранено для расчета следующего, таким образом, оно должно храниться в переменной, чей срок жизни включает в себя все вызовы генератора случайных чисел. На практике это обычно самый внешний блок программы. Значит объявление такой переменной далеко выносится за пределы места её реального использования.

Вот один пример. Это процедура выравнивания текста по ширине из программы, которая описана в главе 7 книги «Software tools». Здесь переменная dir указывает, с какого конца строки надо начинать вставлять дополнительные пробелы; в каждой следующей строке направление меняется на противоположное. На Паскале код выглядит так:

program formatter (...);

var
  dir : 0..1; { с какой стороны начинать добавлять пробелы }
.
.
.
procedure justify (...);
begin
  dir := 1 - dir; { меняем направление на противоположное }
...
  end;

...

begin { главная процедура форматирования }
  dir := 0;
...
  end;

Объявление, инициализация и использование переменной «dir» разбросаны по всей программе, разделенные буквально сотнями строк. В Си или Фортране переменная «dir» могла бы быть частной всего для одной процедуры, которой она нужна:

...

main()
{
...
}

...

justify()
{
    static int dir = 0;

    dir = 1 - dir;
...
}

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

Есть еще как минимум две связанные проблемы. Паскаль не предоставляет способа инициализации переменных статически (т.е. на этапе компиляции). Нет ничего подобного конструкции DATA в Фортране или инициализации в Си:

int dir = 0;

Это означает, что программа на Паскале должна содержать явные выражения присваивания для инициализации переменных. Вроде

dir := 0;

выше. Этот код делает исходный текст программы больше, а также делает больше саму программу во время выполнения.

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

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

Новый стандарт не предлагает статическим переменных, инициализации или не-иерархического способа связи.

2.3. Связанные компоненты программы должны храниться раздельно

Поскольку изначально Паскаль был реализован однопроходным компилятором, язык полагает, что объявление идет строго до использования. В частности, процедуры и функции должны быть объявлены (тело и всё остальное) перед их использованием. В результате типичная программа на Паскале читается «снизу вверх» — все процедуры и функции оказываются перед любым кодом, использующим их, на всех уровнях. Это полностью противоположно порядку, в котором функции разрабатываются и используются.

В некоторой степени это можно смягчить механизмом вроде возможности #include в Си и Ратфоре: файл с исходным кодом может быть включён там, где необходимо, без захламления программы. #include не является частью стандартного Паскаля, хотя компиляторы UCB, VU и Whitesmiths предоставляют такую возможность.

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

Связанная с этим проблема заключается в том, что Паскаль имеет строгий порядок, в котором должны идти объявления. Каждая процедура или функция состоит из:

label — объявление меток, если они используются
const — объявление констант, если они необходимы
type — объявление типов, если необходимо
var — объявление переменных, если они используются
объявление процедур и функций, если они есть
begin
тело функции или процедуры
end

Это означает, что все объявления одного вида (например, типы) должны быть сгруппированы вместе для удобства компилятора, даже когда программист хочет сохранить некоторые вещи логически связанными для лучшего понимания программы. Поскольку программа передается компилятору целиком, редко удается сохранить объявление, инициализацию и использование типов и переменных рядом друг с другом. Даже некоторые наиболее преданные приверженцы Паскаля согласны со мной (14):

«Невозможность создания подобных группировок при структурировании больших программ — одно из наиболее разочаровывающих ограничений Паскаля.»

Возможность включения файлов в этом помогает совсем немного.

Новый стандарт не смягчает требования к порядку объявлений.

2.4. Отсутствие отдельной компиляции

«Официальный» язык программирования Паскаль не предоставляет отдельной компиляции, и в каждой реализации в индивидуальном порядке решается, что делать. Некоторые (например, интерпретатор Беркли) полностью её запрещает, это наиболее близко к духу языка и в точности соблюдает его букву. Многие другие предоставляют объявление, указывающее, что тело функции определено вне программы. В любом случае, все эти механизмы нестандартны, и выполнены по разному в разных системах.

Теоретически отдельная компиляция не нужна — если компилятор очень быстр (и исходный код всех процедур всегда доступен, и компилятор имеет возможность подключения файлов, так что множественное копирование исходного кода не требуется), полная рекомпиляция равноценна. На практике, конечно же, компиляторы никогда не бывают достаточно быстры, и исходный код зачастую скрыт, и подключение файлов не является частью языка, так что изменения отнимают много времени.

Некоторые системы позволяют отдельную компиляцию, но не поддерживает соответствие типов при пересечении границы. Это создает гигантскую дыру в строгой типизации. (Большинство остальных языков также не делают проверку при кросс-компиляции, так что Паскаль не худший в этом отношении) Я видел как минимум одну статью (к счастью, не опубликованную), которая на странице N осуждала Си за отсутствие проверки типов при пересечении границ отдельных компиляций и, на странице N + 1, предлагала способ совладать с Паскалем — компилировать процедуры раздельно, чтобы избежать проверки типов.

Новый стандарт не предлагает отдельной компиляции.

2.5. Несколько разных проблем типов и областей видимости

Большинство следующих вопросов — незначительные раздражители, но я должен их где-нибудь вставить.

В Паскале не разрешено буквально передавать не базовый тип процедуре в качестве параметра. Например, вот так писать нельзя:

procedure add10 (var a : array [1..10] of integer);

Вместо этого необходимо придумать имя типа, сделать объявление типа, и задать формальный параметр как представителя этого типа:

type a10 = array [1..10] of integer;
...
procedure add10 (var a : a10);

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

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

Также несколько беспокоит то, что массивы по умолчанию передаются по значению — в конечном результате каждый параметр, принимающий массив, объявляется программистом с «var» более или менее бездумно. Если нечаянно опустить «var», получится коварный баг.

Конструкция «set» в Паскале кажется хорошей идеей, дающей удобство обозначений и немного бесплатной проверки типов. Например, набор таких проверок:

if (c = blank) or (c = tab) or (c = newline) then ...

может быть написан гораздо более ясно и, возможно, более эффективно:

if c in [blank, tab, newline] then ...

Но на практике перечисляемые типы неприменимы для большего, чем этот пример, поскольку размер перечисления жестко зависит от реализации (возможно потому, что так было в оригинальной реализации CDC: 59 бит). Например, естественно написать функцию «isalphanum(c)» («является ли C буквенно-цифровым символом?») так:

{ isalphanum(c) -- true, если C является буквой или цифрой }

function isalphanum (c : char) : boolean;
begin
  isalphanum := c in ['a'..'z', 'A'..'Z', '0'..'9']
  end;

Но во многих реализациях Паскаля (включая оригинальную) этот код не работает просто потому, что перечислимые типы слишком малы. Следовательно, в основном лучше не использовать перечислимые типы при желании писать программы, независимые от конкретной реализации. (Также этот специфический код работает на порядок медленнее с перечислениями, чем с проверкой диапазонов или наличия в массиве)

2.6. Выхода (приведения типов) нет

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

3. Управляющая логика

Недостатки управляющей логики Паскаля незначительны, но многочисленны — это смерть от множественных ран в отличие от единственного удара в жизненно важную точку.

Нет гарантии порядка выполнения логических операторов «and» и «or» — ничего подобного операторам && и || в Си. Этот недостаток, разделяемый большинством других языков, чаще всего вредит в управлении циклами:

while (i <= XMAX) and (x[i] > 0) do ...

Это крайне неблагоразумное использование Паскаля, поскольку нет уверенности в том, что i будет проверено перед проверкой x[i].

Кстати, скобки в этом коде обязательны — в языке есть только четыре уровня приоритета операторов.

Нет оператора «break» для выхода из циклов. Это соответствует философии «один вход — один выход», поддерживаемой сторонниками структурного программирования, но приводит к отвратительной многоречивости или дублированию кода. В частности, когда это складывается с невозможностью контролировать порядок вычисления логических выражений. Рассмотрим такую ситуацию, выраженную на Си или Ратфоре:

while (getnext(...)) {
    if (<условие>)
        break
    <остаток цикла>
}

Без оператора «break» первой попыткой на Паскале является:

done := false;
while (not done) and (getnext(...)) do
  if <условие> then
    done := true
  else begin
    <остаток цикла>
  end

Но это не работает, поскольку нет способа заставить «not done» выполняться до следующего вызова «getnext». Это приводит, после нескольких фальшстартов, к:

done := false;
while not done do begin
  done := getnext(...);
  if <условие> then
    done := true
  else if not done then begin
    <остаток цикла>
    end
  end

Конечно, рецидивисты могут использовать «goto» и метку (она должна быть объявлена и состоять из цифр) для выхода из цикла. Иначе говоря, ранние выходы болезненны, почти всегда требуется измышление булевой переменной и некоторая изворотливость. Сравните программу поиска последнего непустого члена массива на Ратфоре:

for (i = max; i > 0; i = i - 1)
    if (arr(i) != ' ')
        break

и на Паскале:

done := false;
i := max;
while (i > 0) and (not done) do
  if arr[i] = ' ' then
    i := i - 1
  else
    done := true;

Индекс цикла «for» не определён вне цикла, следовательно, невозможно понять, достиг он конечного значения или нет. Шаг индекса цикла «for» может быть только +1 или -1, что тоже является некоторым ограничением.

Нет оператора «return», опять же по соображениям входа-выхода. Функция возвращает значение установкой значения псевдо-переменной (как в Фортране) и прохождением до конца функции. Иногда это приводит к искажениям для получения уверенности в том, что все пути действительно приводят к концу функции с верным значением. Также нет стандартного пути прервать выполнение иначе, чем достижением конца самого внешнего блока, хотя многие реализации предоставляют оператор «halt», который приводит к немедленной остановке.

Оператор «case» спроектирован лучше, чем в Си, за исключением того, что здесь нет условия «default», и поведение программы не определено, если входное выражение не удовлетворяет ни одному из условий. Это ключевое упущение делает конструкцию «case» почти полностью бесполезной. Среди всех 6000 строк «Software Tools», написанных на Паскале, я использовал её всего 4 раза, хотя при наличии условия «default» оператор «case» можно было бы применить как минимум в дюжине мест.

Новый стандарт не предлагает решения ни одного из этих пунктов.

4. Окружение

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

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

write('Пожалуйста, введите ваше имя: ');
read(name);
...

упреждающее чтение приводит к зависанию программы, ждущей ввода перед выводом подсказки, требующей его.

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

Такой дизайн ввода-вывода отражает оригинальную систему, на которой Паскаль был разработан. Даже Вирт признает эту предвзятость, но не недостатки (15). Подразумевается, что текстовые файлы состоят из записей, то есть строк текста. Когда прочитан последний символ строки, встроенная функция «eoln» становится истинной, в этот момент необходимо вызвать «readln», чтобы инициировать чтение следующей строки и сбросить «eoln». Аналогично, когда последний символ файла прочитан, встроенная функция «eof» становится истинной. В обоих случаях, и «eoln» и «eof», должны проверяться до каждого вызова «read», а не после.

В связи с этим надо приложить значительные усилия для организации корректно работающей системы ввода. Это реализовано в функции «getc», работающей в подсистемах ввода-вывода компиляторов Беркли и VU, но не обязательно будет работать где-либо ещё:

{ getc -- считывает символы со стандартного ввода }

function getc (var c : character) : character;
  var
    ch : char;

  begin
    if eof then
      c := ENDFILE
    else if eoln then begin
      readln;
      c := NEWLINE
      end
    else begin
      read(ch);
      c := ord(ch)
      end;
    getc := c
    end;

Тип 'character' — это не тоже самое, что и тип 'char'. Поэтому ENDFILE и, возможно, NEWLINE не являются допустимыми значениями для переменной 'char' типа.

Здесь вообще нет упоминания о доступе к файловой системе, за исключением предопределенных файлов, с именем (фактически) по номеру логического устройства в операторе «program», начинающим каждую программу. Это, по всей видимости, отражает пакетную систему компьютера CDC, на котором Паскаль был изначально разработан. Переменная типа «файл»:

var fv : file of type

очень особенный объект — он не может быть ни присвоен, ни использоваться иначе, как в вызовах встроенных процедур вроде «eof», «eoln», «read», «write», «reset» and «rewrite». («reset» переводит в начало файла и делает его доступным для нового чтения, «rewrite» делает файл доступным для записи.)

Большинство реализаций Паскаля предоставляют «аварийный люк» для возможности доступа к файлам по имени из внешнего окружения, но это не удобно и не стандартно. Например, множество систем допускают аргумент filename с именем файла в вызовах «reset» и «rewrite»:

reset(fv, filename);

Но «reset» и «rewrite» — процедуры, не функции. Они не возвращают статус и нет способа восстановить контроль, если по какой-либо причине попытка доступа провалилась. (UCSD предоставляет флаг времени компиляции, который отключает обычное аварийное завершение) Так как переменная fv не может появиться в выражениях вида:

reset(fv, filename);
if fv = failure then ...

в этом направлении также нет выхода. Эта смирительная рубашка, по существу, делает невозможным написание программ, восстанавливающихся при опечатках в именах файлов и т.д. Я так и не решил эту проблему адекватно в «Software Tools».

Нет упоминания о доступе к аргументам командной строки, возможно это снова отражение пакетной обработки истоков Паскаля. Локальные процедуры могут предоставить эту возможность путём добавления нестандартных процедур к окружению.

Поскольку на Паскале невозможно написать универсальную программу распределения памяти (невозможно описать тип данных, который вернет такая функция), язык имеет встроенную процедуру «new», которая выделяет память из «кучи». Память может быть выделена только для заранее определенных типов, поэтому невозможно выделить память, например, для массивов произвольных размеров, содержащих строки. Указатели, возвращаемые оператором «new», могут быть переданы, но не изменены: нет арифметики указателей. Нет способа возвращения контроля, если хранилище исчерпается.

Новый стандарт не предполагает изменений в какой-либо из этих областей.

5. Косметические проблемы

Большинство этих проблем утомительны для опытных программистов, а некоторые, возможно, мешают даже новичкам. Со всеми из них можно жить.

Паскаль, вместе с большинством языков, вдохновленных Алголом, использует точку с запятой скорее как разделитель выражений, а не их ограничитель (как в PL/I и Си). В результате необходимо достаточно глубокое знакомство с выражением для верного расположения точек с запятой. Возможно более важно то, что при внимании к верному использованию необходимо значительное количество нудного редактирования. Предположим, что первая редакция программы выглядит так:

if a then
  b;
c;

Но если необходимо что-либо вставить перед b, то точка с запятой более не нужна, поскольку теперь это выражение предшествует оператору «end»:

if a then begin
  b0;
  b
  end;
c;

Если теперь мы добавим ветку «else», необходимо удалить точку с запятой после «end»:

if a then begin
  b0;
  b
  end
else
  d;
c;

И так далее, точки с запятыми будут появляться и исчезать с развитием программы.

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

Программисты Си и Ратфора находят «begin» и «end» громоздкими в сравнении с { и }.

Имя функции само по себе является вызовом этой функции. Нет способа отличить такой вызов функции от простой переменной, за исключением знания имен функций. Паскаль использует трюк Фортрана — имя функции внутри неё работает как переменная, за исключением того, что в Фортране имя функции действительно является переменной и может появляться в выражениях, в Паскале же её появление в выражении выражает рекурсивный вызов: если f — функция без аргументов, то «f := f + 1» является рекурсивным вызовом f.

Есть нехватка операторов (возможно это связано с нехваткой уровней порядка предшествования выполнения операторов). В частности, нет операторов работы с битами (AND, OR, XOR и т.д.). Я просто сдался в попытках написать на Паскале такую тривиальную программу шифрования:

i := 1;
while getc(c) <> ENDFILE do begin
  putc(xor(c, key[i]));
  i := i mod keylen + 1
  end

Поскольку я не могу написать практичную функцию «xor». Перечислимые типы здесь немного помогают (так сказать), но недостаточно; люди, которые заявляют, что Паскаль — язык системного программирования, вообще просмотрели этот момент. Например, [18, с. 685]:

«В настоящее время [1977] Паскаль — лучший язык из общедоступных для целей системного программирования и реализации программного обеспечения.»

кажется несколько наивным.

Нет пустой строки, возможно потому, что Паскаль использует удвоение кавычки для обозначения кавычки в строке:

'Это символ '' '

Нет способа включать неграфические символы в строки. По сути, неграфические символы — персоны нон-грата в сильнейшем смысле, поскольку они не упомянуты ни в одной части стандартного языка. Переводы строк, табуляция и так далее выводятся в каждой системе частным образом, обычно на основании знаний о кодировке (например, в ASCII перевод строки имеет десятичное значение 10).

Нет макропроцессора. Механизм «const» для объявления явных констант заботится о примерно 95% применений простого оператора #define в Си, но более сложные варианты безнадежны. Безусловно возможно включить макро-препроцессор в компилятор Паскаля. Это позволит мне имитировать удобную процедуру «error» для ошибок:

#define error(s) begin writeln(s); halt end

(Необходимо определить «halt» как путь к концу самого внешнего блока) Далее вызовы:

error('короткая строка');
error('строка гораздо длиннее');

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

Язык запрещает выражения в объявлениях, так что невозможно написать подобные вещи:

const SIZE = 10;
type arr = array [1..SIZE+1] of integer;

Или даже более простые:

const SIZE = 10;
SIZE1 = SIZE + 1;

6. Перспективы

Попытка переписать программы «Software Tools» началась в марте 1980 года, и продвигаясь скачками, длилась до января 1981 года. Конечный продукт (19) был опубликован в июне 1981. В течение этого времени я постепенно приспособился к большинству поверхностных проблем Паскаля (косметическим, недостаточности управления выполнением) и разработал несовершенные решения значительных проблем (размеры массивов, окружение времени выполнения).

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

Нет значительных направлений, в которых я сочту Паскаль превосходящим Си, но есть несколько мест, которые явно улучшены по сравнению с Ратфором. Наиболее очевидна, несомненно, рекурсия: некоторые программы гораздо яснее при рекурсивной записи, в частности поиск по шаблону, быстрая сортировка, вычисление выражений.

Перечислимые типы данных — хорошая идея. Они одновременно разграничивают диапазон допустимых значений и документируют их. Записи помогают группировать связанные переменные. Я нахожу относительно мало применений для указателей.

Булевые переменные лучше, чем целые числа для логических условий; оригинальные программы на Ратфоре содержали некоторые искусственные конструкции, поскольку логические переменные Фортрана плохо сделаны.

Иногда проверка типов Паскаля предупредит об упущениях в написании программы; проверка значений времени выполнения также покажет ошибки время от времени, в частности, выход за границы индексов.

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

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

Законченные программы обычно имеют примерно то же число строк кода, что и эквивалентные на Ратфоре. Сперва это удивило меня, поскольку у меня было предвзятое мнение о Паскале как многословном и менее выразительном языке. Настоящая же причина, кажется, в том, что Паскаль разрешает произвольные выражения в таких местах, как границы циклов и индексы, где Фортран (то есть Фортран 66) не позволяет, так что некоторые бесполезные присвоения могут быть исключены. Более того, программы на Ратфоре определяют функции там, где в Паскале это не нужно.

В завершение, позвольте мне подвести итог главных пунктов против Паскаля.

  1. Поскольку размер массива — часть его типа, невозможно написать универсальные процедуры для работы с массивами различной длины. В частности, очень трудна обработка строк.
  2. Отсутствие статических переменных, инициализации и способа не-иерархичной связи объединяются в уничтожении «локальности» программы — переменные требуют гораздо более широкой области видимости, чем должны.
  3. Однопроходная сущность языка принуждает к наличию процедур и функций в неестественном порядке; вынужденное разделение различных объявлений разбрасывает компоненты программы, которые логически принадлежат друг другу.
  4. Отсутствие раздельной компиляции препятствует разработке больших программ и делает использование библиотек невозможным.
  5. Порядок вычисления логических выражений не поддается управлению, что приводит к извилистому коду и посторонним переменным.
  6. Оператор «case» выхолощен отсутствием выражения по умолчанию.
  7. Стандартный ввод/вывод дефективен. Нет практичного обеспечения для работы с файлами или аргументами программы как части языка и нет механизма расширения.
  8. В языке не хватает большинства инструментов, необходимых для сборки больших программ, в особенности включения файлов.
  9. Выхода (приведения типов) нет.

Последний пункт возможно наиболее важен. Язык неадекватный и ограниченный, поскольку нет способа преодолеть его ограничения. Нет возможности отключить проверку типов при необходимости. Нет способа заменить дефективное окружение времени выполнения практичным, пока не получить контроль над компилятором, задающим «стандартные процедуры». Язык замкнут.

Люди, выбравшие Паскаль для серьёзного программирования, попали в смертельную ловушку.

Поскольку язык настолько беспомощен, он должен быть расширен. Но каждая группа расширяет Паскаль в своем собственном направлении, чтобы сделать его похожим на какой-то язык, который они действительно хотят. Расширения для раздельной компиляции, COMMON по типу Фортрана, строковые типы данных, внутренние статические переменные, инициализация, восьмеричные числа, побитовые операторы и т.д. — всё добавляется для полезности языка в одной группе, но уничтожает переносимость для других.

Я чувствую, что ошибочно использовать Паскаль для чего-либо, далёкого от его изначальной цели. В чистом виде Паскаль — игрушечный язык, пригодный для обучения, но не реального программирования.

Благодарности

Я благодарен Элу Ахо, Элу Фьюэру, Нараин Джехани, Бобу Мартину, Дугу Макилрою, Робу Пайку, Дэннису Ричи, Крису Ван Вайку и Чарльзу Уэзереллу за полезную критику ранних версий этой статьи.

[1]
Feuer, A. R. and N. H. Gehani, «A Comparison of the Programming Languages C and Pascal — Part I: Language Concepts», Bell Labs internal memorandum (September 1979).
[2]
N. H. Gehani and A. R. Feuer, «A Comparison of the Programming Languages C and Pascal — Part II: Program Properties and Programming Domains», Bell Labs internal memorandum (February 1980).
[3]
P. Mateti, «Pascal versus C: A Subjective Comparison», Language Design and Programming Methodology Symposium, Springer-Verlag, Sydney, Australia (September 1979).
[4]
A. Springer, «A Comparison of Language C and Pascal», IBM Technical Report G320–2128, Cambridge Scientific Center (August 1979).
[5]
B. W. Kernighan and P. J. Plauger, Software Tools, Addison-Wesley, Reading, Mass. (1976).
[6]
Кэтлин Йенсен, Никлаус Вирт, Паскаль: руководство для пользователя — М.: Компьютер, 1993.
[7]
David V. Moffat, «A Categorized Pascal Bibliography», SIGPLAN Notices 15(10), pp. 63–75 (October 1980).
[8]
A. N. Habermann, «Critical Comments on the Programming Language Pascal», Acta Informatica 3, pp. 47–57 (1973).
[9]
O. Lecarme and P. Desjardins, «More Comments on the Programming Language Pascal», Acta Informatica 4, pp. 231–243 (1975).
[10]
H. J. Boom and E. DeJong, «A Critical Comparison of Several Programming Language Implementations», Software Practice and Experience 10(6), pp. 435–473 (June 1980).
[11]
N. Wirth, «An Assessment of the Programming Language Pascal», IEEE Transactions on Software Engineering SE-1(2), pp. 192–198 (June, 1975).
[12]
O. Lecarme and P. Desjardins, ibid, p. 239.
[13]
A. M. Addyman, «A Draft Proposal for Pascal», SIGPLAN Notices 15(4), pp. 1–66 (April 1980).
[14]
J. Welsh, W. J. Sneeringer, and C. A. R. Hoare, «Ambiguities and Insecurities in Pascal», Software Practice and Experience 7, pp. 685–696 (1977).
[15]
N. Wirth, ibid., p. 196.
[16]
J. D. Gannon and J. J. Horning, «Language Design for Programming Reliability», IEEE Trans. Software Engineering SE-1(2), pp. 179–191 (June 1975).
[17]
J. D. Ichbiah, et al, «Rationale for the Design of the Ada Programming Language», SIGPLAN Notices 14(6) (June 1979).
[18]
J. Welsh, W. J. Sneeringer, and C. A. R. Hoare, ibid.
[19]
B. W. Kernighan and P. J. Plauger, Software Tools in Pascal, Addison-Wesley (1981).

http://translatedby.com/you/why-pascal-is-not-my-favorite-programming-language/into-ru/trans/
Оригинал (английский): Why Pascal is Not My Favorite Programming Language, сохранённая копия
Перевод: © Fenrir, Softy, denton, the_corrector, Maximusya, Владимир Ступин, mkazantsev, renatvn, Stam, brawaga, Владимир, dmitry.
translatedby.com переведено толпой
HTML–разметка, немного правки Гиви Хакеридзе (август 2022 года)