пятница, 5 октября 2012 г.

Break или не break?


В общении с коллегами — учителями информатики — я много раз сталкивался с твёрдым убеждением, что использование оператора break для досрочного выхода из цикла — это «неграмотно», «грязный хак», «не соответствует принципам структурного программирования» и вообще «я своим ученикам такое не зачту». В то же время общение с профессиональными программистами показывает, что такой прием на практике применяется очень часто, потому что это удобно и в большинстве случаев делает программу более понятной.

Такой «разброд» имеет совершенно объяснимые причины. Большинство преподавателей, с одной стороны, когда-то заучили, что структурное программирование — это хорошо, а любое отступление от него — это плохо. С другой стороны, сами они уже фактически отошли от промышленного программирования: я могу на пальцах одной руки пересчитать знакомых (в том числе и по Интернету) учителей информатики, которые сами написали что-то стóящее. Таким образом, наблюдаем закон Дж.Б. Шоу в действии.

Попробуем разобраться в сути вещей. Оператор break — это фактически оператор перехода, знаменитый GOTO, который в 1970-е годы был морально уничтожен, прежде всего, стараниями «отца структурного программирования» Эдсгера Дейкстры [1]. Однако сами «отцы» хорошо понимали, что программа без GOTO ещё не становится автоматически структурной программой. Поэтому появилось на свет эссе Дональда Кнута «Структурное программирование с операторами GOTO» [2]. Кнут писал (перевод мой):

«Другими словами, мы не должны просто удалять операторы GOTO из-за того, что сейчас модно это делать; присутствие или отсутствие операторов GOTO — это не главный вопрос. Истинная цель состоит в том, чтобы формулировать наши программы таким образом, чтобы их было легко понимать.»

Посмотрим, как обстоит дело с пониманием программ, содержащих циклы с операторами break и сравним их с программами, где операторов break умышленно избегают.

Пример 1. С клавиатуры вводятся числа, ввод заканчивается числом 999. Вычислить сумму введенных чисел.

Вот решение с использованием break:
s := 0;
while True do begin
  Readln(x);
  if x = 999 then break;
  s := s + x;
end;
На взгляд автора, этот алгоритм очень хорошо соответствует словесному описанию: «строим цикл, выход из которого происходит при получении числа 999».

Теперь посмотрим на «кошерные» альтернативы. Нужно как-то выполнить те же действия (то есть, выйти из цикла при получении числа 999), не используя оператор выхода из цикла.

Во-первых, можно поставить условие цикла x <> 999, но при этом нужно будет инициализировать переменную x до цикла каким-то «магическим числом», отличным от 999, например, 1 (или 998 :-). Программист, который будет разбираться в таком коде через некоторое время, спасибо вам не скажет. Кроме того, вторую часть тела цикла придется взять в условный оператор, а эта вторая часть может быть достаточно большой. Читабельность явно не повышается. Зато программа «структурная», можно «взять с полки пирожок». :-)
s:=0;
x := 1;
while x <> 999 do begin
  Readln(x);
  if x <> 999 then
    s := s + x;
end;
Заметим, что число 999 появляется в программе дважды: в заголовке цикла и в условии в теле цикла, что само по себе уже нехорошо.

Можно вынести оператор Readln за цикл, продублировав его в теле цикла:
s := 0;
Readln(x);
while x <> 999 do begin
  s := s + x;
  Readln(x);
end;
На взгляд автора, при этом два оператора ввода, выполняющие одну и ту же функцию, «размывают» логику программы и не добавляют ей «прозрачности». Кроме того, вместо Readln в других ситуациях может стоять целая группа операторов, и тут уже дублирование будет выглядеть совсем некрасиво.

Кроме того, можно ввести логическую переменную (флаг), которой присваивается истинное значение при получении числа 999:
stop := False;
while not stop do begin
  Readln(x);
  if x = 999 then
    stop := True
  else
    s := s + x;
end;
Но тут нужно вспомнить о «бритве Оккама» — не плоди лишних сущностей. И ещё — любое усложнение системы, как правило, снижает её надежность.

Еще один вариант — перейти на цикл с постусловием:
s := 0;
repeat
  Readln(x);
  if x <> 999 then
    s := s + x;
until x = 999;
Во-первых, как и в одном из предыдущих вариантов, здесь два раза всплывает число 999. Во-вторых, вторую часть тела цикла снова нужно помещать в условный оператор. В-третьих, читать программы с циклами repeat — это сущее наказание: встретив слово repeat, судорожно пытаемся найти соответствующий until с условием, без этого всё вообще непонятно. Потом опять нужно смотреть наверх: что же там в теле цикла...

Можно, конечно, и так :-):
s := 0;
try
  while True do begin
    Readln(x);
    if x = 999 then raise Exception.Create('');
    s := s + x;
  end;
except end;
Но по сути это то же самое, что и break. Использовать здесь исключения — всё равно, что гвозди микроскопом забивать.

Рассмотрим еще один пример.

Пример 2. Найти в массиве A[1..N] элемент, равный X, или сообщить, что такого элемента нет.

Решение с помощью break:
nX := 0;
for i:=1 to N do
  if A[i] = X then begin
    nX := i;
    break;
  end;
if nX = 0 then
     writeln('Не нашли!')
else writeln('nX = ', nX);
Как и для первого примера, программа с break почти дословно повторяет словесное описание идеи решения: «просматриваем все элементы массива с первого до последнего; как только нашли нужный элемент, запоминаем его номер и выходим из цикла».

Вот альтернатива без break:
nX := 1;
while (nX <= N) and (A[nX] <> X) do
  Inc(nX);
if nX > N then
     writeln('Не нашли!')
else writeln('nX = ', nX);
Теперь представим себе, что будет, если в трансляторе включена проверка выхода за границы массива, логические выражения вычисляются полностью и элемента, равного X, в массиве нет: программа вылетит в результате обращения за пределы массива.

Вот еще вариант, с логической переменной:
nX := 1;
found := False;
repeat
  if A[nX] = X then
       found:= True
  else Inc(nX);
until found or (nX > N);
if not found then
     writeln('Не нашли!')
else writeln('nX = ', nX);
Какая программа понятнее, каждый решает сам. :-)

Итак, подытожим.
  1. Оператор break есть практически во всех современных языках программирования. «Значит, это кому-нибудь нужно!»
  2. Это инструмент, который нужно использовать в соответствии с его назначением.
  3. Само по себе наличие или отсутствие оператора break ничего не говорит о том, грамотно ли написана программа; задача состоит в том, чтобы сделать ее наиболее понятной и «прозрачной».
  4. Использование оператора break относится к так называемым «структурным» переходам [3], то есть к переходам вперёд в пределах того же модуля, что не нарушает принципы структурного программирования.

Разобравшись с break, можно перейти к его непосредственному «родственнику» — оператору continue, который передает управление сразу в конец цикла, переходя к следующему шагу, если требуется.

Пример 3. В цикле обрабатываются все элементы массива A[1:N]. Для каждого из них сначала выполняются операторы S1, S2, ..., SN, в результате вычисляется некоторое значение x. Если получено значение x = 1, нужно выполнить еще операторы T1, T2, ..., TM, а в противном случае перейти к следующему элементу.

«Классическое» решение выглядит так:
for i:=1 to N do begin
  S1;
  S2;
  ...
  x := SN;
  if x = 1 then begin
    T1;
    T2;
    ...
    TM;
  end
end;
Вроде бы всё хорошо. Но мы потеряли «локальность»: при достаточно длинном теле условного оператора нужно еще «сканировать» цикл до конца, проверяя, не выполняются ли какие-то действия в том случае, когда x <> 1. А эту проблему может элегантно решить continue:
for i:=1 to N do begin
  S1;
  S2;
  ...
  x := SN;
  if x <> 1 then continue;
  T1;
  T2;
  ...
  TM
end;
Здесь уже точно ясно, что при x <> 1 никаких дополнительных операций не происходит. По мнению автора, такой вариант более «прозрачен» и, по крайней мере, не хуже предыдущего.

Остается еще один «смежный» вопрос: можно ли писать подпрограммы с несколькими выходами? Давайте посмотрим пример рекурсивной процедуры.

Пример 4. Разработать процедуру, которая выводит на экран решение задачи «Ханойская башня» [4].

Рекурсивное решение этой задачи хорошо известно, одна из реализаций выглядит так:
procedure Hanoi(n, k, m: integer);
var p: integer;
begin
  if n = 0 then exit;
  p := 6 - k - m;
  Hanoi(n-1, k, p);
  writeln(k, ' -> ', m);
  Hanoi(n-1, p, m)
end;
Здесь n — количество дисков, которые нужно перенести; k — номер начального стержня и m — номер конечного стержня.

И все было бы хорошо, но тут нарушен ещё один принцип, который стал «священной коровой» ортодоксального структурного программирования: процедура имеет (увы :-) два выхода, один естественный, и второй — по признаку окончания рекурсии.

Можно ли было обойтись без этого? Да, можно, «упаковав» все тело процедуры внутрь условного оператора:
procedure Hanoi(n, k, m: integer);
var p: integer;
begin
  if n > 0 then begin
    p := 6 - k - m;
    Hanoi(n-1, k, p);
    writeln(k, ' -> ', m);
    Hanoi(n-1, p, m)
  end
end;
Но тут мы опять теряем локальность — при достаточно длинном тексте процедуры нужно еще посмотреть, не выполняются ли какие-то действия после условного оператора. Да и не совсем понятно, зачем добавлять лишний уровень вложенности. «Не плоди лишних сущностей». По мнению автора, первое приведенное решение более понятное и более красивое.

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

Будем предполагать, что введен тип данных
type Matrix = array[1..N,1..N] of integer;
Тогда функцию можно написать так:
function Find(const A: Matrix; X: integer): boolean;
var i, j: integer;
begin
  Result := True;
  for i:=1 to N do
    for j:=1 to N do
      if A[i,j] = X then exit;
  Result := False
end;
Здесь оператор exit фактически выполняет роль break для вложенного цикла. С формальной точки зрения у функции два выхода. В общем, «я своим ученикам никогда такое не зачту» — уже слышу я от учителей информатики.

А давайте подумаем, какие альтернативы? Можно вот так:
function Find(const A: Matrix; X: integer): Boolean;
var i, j: integer;
label finish;
begin
  Result := True;
  for i:=1 to N do
    for j:=1 to N do
      if A[i,j] = X then goto finish;
  Result := False;
finish:
end;
Но тут вообще «криминал» — метка и goto! Хотя по сути ничего не изменилось, тот же выход из вложенного цикла.

Конечно, здесь можно использовать исключение:
function Find(const A: Matrix; X: integer): Boolean;
var i, j: integer;
begin
  Result := False;
  try
    for i:=1 to N do
      for j:=1 to N do
        if A[i,j] = X then
          raise Exception.Create('');
  except
    Result := True
  end
end;
Но ведь это далеко не «исключительная ситуация», поэтому по смыслу исключения здесь всё-таки не особо уместны. Кроме того, нужно учитывать, что при обработке исключений выполняется большое число машинных команд, что снижает эффективность программы.

Любителей «чистого» структурного программирования наверняка устроит такой вариант решения:
function Find(A: Matrix; X: integer): Boolean;
var i, j: integer;
begin
  Result := False;
  for i:=1 to N do
    for j:=1 to N do
      if A[i,j] = X then Result := True
end;
Но, к сожалению, по эффективности он не может составить конкуренцию предыдущим, так как в любом случае рассматриваются все N2 элементов массива, даже если элемент A[1,1] равен X.

Выводы

Эта статья, прежде всего, о том, что любые идеи и принципы нужно воспринимать в контексте конечной цели. Одна из основных целей концепции структурного программирования — повысить читаемость программ и за счёт этого облегчить их отладку, тестирование и сопровождение. Поэтому применение операторов break, continue и exit нельзя считать отступлением от структурного программирования, если это не ухудшает читаемость программы и не приводит к запутыванию логики алгоритма.

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

Литература
  1. Dijkstra E.W., Go To Statement Considered Harmful // Communications of the ACM, Vol. 11, No. 3, March 1968, pp. 147-148. (PDF)
  2. Knuth D.E. Structured Programming with GO TO Statements // ASM Computing Surveys. 1974. 6(4). P.261-301. (HTML, PDF)
  3. Непейвода H.H., Скопин И.Н. Основания программирования. — Москва-Ижевск: Институт компьютерных исследований, 2003. (PDF)
  4. Гарднер М., Математические головоломки и развлечения. — М.: Мир, 1999.

Ярлыки: , , , , , , ,

пятница, 13 апреля 2012 г.

Есть ли operator присваивания в Паскале?

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

В ходе подготовки учебника к экспертизе у редактора возникли сомнения по поводу моей фразы: «Оператор «:=» — это оператор присваивания». По её мнению, оператор присваивания — это всё выражение вида «переменная := выражение;», а символы «:=» — это знак присваивания.

Как оказалось, этот терминологический вопрос совсем не прост. Изучение источников как на русском, так и на английском языках, даёт противоречивые результаты.

Поскольку речь идет о Паскале, обратимся, прежде всего, к отчёту «The Programming Language Pascal», который написал Никлаус Вирт, создатель этого языка. Он пишет:

«The most fundamental statement is the assignment statement. It specifies that a newly computed value be assigned to a variable (or a component of a variable).»

«Assignment statement» — это оператор присваивания, здесь слово «statement» действительно переводится на русский как оператор в значении «инструкция», «команда».

Но вопрос в другом — как назвать символ «:=»? Вирт в своих работах не дает ему никакого названия, такой же подход принят и в стандарте ISO7185.

Часто утверждается, в Паскале вообще нет оператора присваивания, такого как в Си и Си++, потому что присваивание — «это утверждение, а не выражение». Однако существует и другое мнение. Например, в книге М. Канту «Essential Pascal» видим термин «colon-equal operator». А Н. Дейл прямо использует термин «assignment operator» (см. «Programming in Pascal», глава 2).

Итак, мы добрались до сути: «:=» — это оператор или не оператор? Чтобы ответить на этот вопрос, нужно понять, какое значение слова «оператор» имеется в виду. В английском языке используется два совершенно разных слова, которые переводятся на русский одним и тем же словом «оператор» — это «statement» и «operator». С оператором-statement вроде бы всё понятно. Оператор-operator — это другой термин, который связан с математическим понятие оператора — правила преобразования одного объекта в другой.

Выражение «assignment operator» широко используется в таких языках, как Си и Си++, где оператор присваивания обозначается знаком «=». Его суть в том, что оператор присваивания создает новый объект на основе двух существующих объектов, записывая новое значение в переменную.

Таким образом, для того, чтобы разобраться, является ли символ «:=» оператором в этом смысле, нужно выяснить, могут ли при присваивании выполняться какие-то преобразования.

Считается, что в «классическом» Паскале оператора присваивания (в смысле operator) нет. Но это не совсем верно. Вот пример программы, которая работает во всех современных версиях Паскаля:
var a: integer;
    b: longint;
begin
  a := 5;
  b := a;
  b := 25;
  a := b;
end.
Очевидно, что здесь перед записью значения в переменную некоторые преобразования данных (приведение к другому типу) автоматически выполняются.

Кроме того, в современной версии Free Pascal символ «:=» вполне законно называется оператором присваивания, потому что его можно перегружать, как и в Си++. В программе, приведенной ниже, показан пример такой перегрузки.
type
  complex = record re,im: real; end;
var z: complex;
  operator :=(r: double) z: complex;
  begin
    z.re:=r;
    z.im:=0;
  end;
begin
  z:=3;
end.
Выводы: Таким образом, в современном Паскале «X:=Y;»  — это оператор присваивания (statement), а «:=» — это тоже оператор присваивания (operator).

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

Ярлыки: , , ,

пятница, 8 апреля 2011 г.

Кумир и школьная информатика

Как все уже знают, в ближайшие два года будет происходить постепенный переход на компьютеризированный вариант сдачи ЕГЭ по информатике. Компьютерная тестирующая система (КТС ЕГЭ) пока ориентирована на использование кроссплатформенных систем программирования Кумир (Комплект Учебных МИРов, школьный алгоритмический язык) и FreePascal.

Сильный эффект произвело известие об административном внедрении системы Кумир, которая разработана в НИИСИ РАН по заказу Российской Академии Наук и распространяется свободно на условиях лицензии GNU GPL.

Конечно, все «старики», знакомые с учебниками информатики А.Г. Кушниренко 1990 года, знали о Кумире, кое-где даже использовали его DOS-версию, но широкого распространения (по крайней мере, в Питере) она никогда не имела по разным причинам, в первую очередь, из-за убогого интерфейса.

Первый сигнал о втором пришествии Кумира появился в апреле 2010 года после семинаров, которые проводили П.А. Якушкин, В.Р. Лещинер, А.Г. Кушниренко и М.А. Ройтберг на Дне учителя информатики 2.04.2010 в рамках Девятого московского педагогического марафона. Чуть позже в газете «Информатика» появилась статья В.Р. Лещинера и П.А. Якушкина, посвященная компьютерному варианту ЕГЭ.

Новость произвела эффект разорвавшейся бомбы среди учителей информатики. На педагогических сайтах развернулись бурные дискуссии (см. тему «Быть Кумиру?» и обсуждение на сайте pedsovet.org).
Большинство участников этих словесных баталий Кумир не приняли, было сказано много ругательных слов.

Автору этих строк Кумир тоже никогда не нравился, хотя нравились идеи С. Пейперта, Г.А. Звенигородского и учебник информатики А.Г. Кушниренко 1990 года. Такой вот парадокс. В результате была написана среда «Исполнители», которая успешно используется с 1992 года по сей день во многих школах. Кстати, в начале 90-х журнал «Информатика и образование» отклонил статью, посвященную «Исполнителям», только из-за того, что «среда Кумир уже получила широкое распространение и альтернативы не актуальны».

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

Но теперь встал вопрос о том, что Кумир становится одним из (двух) языков, разрешенных на компьютерном ЕГЭ по информатике, то есть «поднимается» на уровень 10-11 классов. Это заставило задуматься. Мера явно вынужденная: для компьютерного ЕГЭ требуется
  • кроссплатформенность
  • бесплатность
  • простота установки
  • относительная известность и популярность.
Таких сред не очень много, поэтому «архитекторов» нововведений можно понять. В начале этого учебного года стало ясно, что возвращение Кумира — это серьезно, и его проталкивают «сверху». Московский институт открытого образования (МИОО) проводит дистанционный курс Подготовка выпускников к ЕГЭ по информатике и ИКТ в компьютеризированной форме, где Кумиру отводится важнейшая роль. Для просмотра материалов курса требуется бесплатная регистрация, но выложенные материалы того стоят, рекомендую посмотреть. 29 ноября 2010 года тот же МИОО проводит апробацию компьютеризированной системы проведения ЕГЭ по информатике и ИКТ (КТС ЕГЭ). В газете «Информатика» (№№ 24/2010 и 2/2011) публикуются статьи А.Г. Леонова из серии «Освой КуМир за 6 часов». До этого, в 2009 году, напечатан цикл материалов А.Г. Кушниренко и А.Г. Леонова «Методика преподавания основ алгоритмизации на базе системы КуМир». На очередном Дне учителя информатики (01.04.2011) продвижение КуМира продолжается (семинар и круглый стол). Все это говорит о том, что с Кумиром придется считаться. Как и любое явление, он имеет достоинства и недостатки. О них много говорили на упомянутом курсе МИОО, даже была сделана Wiki-страничка «Плюсы и минусы Кумира». Там представлен широкий спектр мнений, поэтому здесь не будем повторяться. Автор в текущем учебном году решил попробовать перейти в 7-8 классах на Кумир. Сразу обнаружились достаточно серьезные проблемы, которые позволяли сделать вывод о непригодности среды в ее текущем состоянии для этих целей. Самые важные среди них:
  • чудовищная медлительность Кумира, работающего в сотни раз медленнее, чем любая из Паскаль-сред
  • отсутствие «нормальной» (не черепашьей) графики
  • совершенно неразвитые средства работы со строками (например, не было функции поиска)
  • устарелая и полностью кривая работа с файлами (например, перед тем, как открыть файл на запись, нужно каждый раз проверять, существует ли он, и если нет, то создавать пустой файл отдельной командой)
К счастью, вскоре удалось выйти на прямую связь с руководителем группы разработчиков Кумира М.А. Ройтбергом. В результате сотрудничества большинство проблем удалось снять, и появилась предварительная версия, в которой можно нормально работать, то есть делать то, что раньше мы делали на Паскале и Си. Медлительность, правда, осталась, но А.Г. Кушниренко обещал, что версия 2.0 будет существенно быстрее (ему, наверное, это обещали программисты). Материалы презентаций к урокам по Кумиру можно взять у меня на сайте (там около 400 слайдов). Самую последнюю версию Кумира можно скачать на сайте разработчиков. Необходимо отметить, что ряд проблем все же остаются в текущей версии (1.8.0):
  • нельзя менять значения аргументов внутри вспомогательных алгоритмов (например, в реализации алгоритма Евклида как функции приходится заводить две лишние переменные);
  • нельзя вызывать функцию как процедуру, игнорируя ее результат (например, когда результат функции — код возврата и в данном случае он меня не интересует);
  • неудобная и неполная справочная система;
  • нет форматного вывода на консоль и в файл, как в Паскале (типа вывод x:4), это нужно, например, чтобы вывести на экран матрицу ровными столбиками.
По словам разработчиков, по крайней мере, некоторые из этих недостатков будут устранены в версии 2.0. Общие выводы по результатам года:
  • русские команды школьники воспринимают намного легче английских
  • для изучения основ программирования и алгоритмизации Кумир ничем не хуже Паскаля, в новой версии можно делать практически все, что нужно; если удастся серьезно ускорить вычисления, будет совсем хорошо
  • Кумир очень неплохо идет даже в 9-11 классах на базовом уровне и может быть очень удачным выбором для тех, кто в будущем не будет профессионально программировать.
Напоследок приведу список сетевых ресурсов по Кумиру:

Ярлыки: , , , ,