Untying Knuth knots: как реструктурировать код спагетти?
Этот вопрос был вдохновлен Как преобразовать блок-схему в реализацию?, в которой говорится о способах алгоритмического устранения инструкции goto
из кода. Ответ на общую проблему описан в этой научной статье.
Я реализовал некоторый код, следуя высокоуровневому эскизу Алгоритма X от Кнута. Искусство компьютерного программирования, описывающее генерацию лексикографических перестановок с ограниченными префиксами (см. стр. 16 этого черновик).
Это соответствующая блок-схема приведенного выше алгоритма.
Это может быть очень умный и очень эффективный алгоритм, но структура кода кажется сложной задачей. Я закончил использование старой старой goto
-стильной реализации:
//Algorithm X;
1:
initialize();
2:
enter_level(k);
3:
set(a[k],q);
if(test() == ok) {
if (k == n) {
visit();
goto 6;
}
goto 4;
}
goto 5;
4:
increase(k);
goto 2;
5:
increasev2(a[k]);
if (q != 0) {
goto 3;
}
6:
decrease(k);
if (k==0) {
goto 7;
}
set(p,u_k);
goto 5;
7:
return;
Возникает вопрос: как этот код может быть реорганизован для устранения всех вызовов goto
?
Один (фиктивный) ответ - это предложение "взглянуть на цитированную научную статью и следовать ей по очереди" - действительно, это, безусловно, возможно. Но этот вопрос связан с тем, что опытные программисты видят сразу, как только они взглянут на этот код спагетти.
Меня интересует как поэтапно рефакторинг, а не только код.
Примечание:
- Прямо реализовать алгоритм X на основе его высокоуровневой спецификации и
goto
переходов. Реализация функций черного ящика initialize()
и т.д. Потребует только нескольких дополнительных инструкций, но они не имеют отношения к структуре кода. Не важно, что происходит во время вызовов функций, так как теперь основное внимание уделяется потоку программы.
- Обычная дискуссия "GOTO по-прежнему считается вредной?" абсолютно не имеет отношения к этому вопросу и не должна быть рассмотрена вообще в ответах и комментариях.
Связано: как работать с или завершить код спагетти?
Ответы
Ответ 1
Я набросал алгоритм для OP ранее fooobar.com/questions/557406/...
Найдена лучшая статья, в которой обсуждается, как сгенерировать структурированный код, сохраняя при этом собственно исходный граф потока управления:
W.D Maurer, "Обобщенные структурированные программы и деревья циклов" , Science of Computer Programming, 2007
Я следовал этой процедуре (на бумаге, надеюсь, я сделал это правильно, посмотрел ОК в 2:40). Его основной трюк состоит в том, чтобы найти сильно связанные области (циклы в коде); они станут петлями; Затем он разрушает этот цикл, удаляя край; это в конечном итоге становится обратной связью цикла (восстанавливается, когда он завершен). Процесс повторяется до тех пор, пока не будет найдено больше циклов; то, что осталось, по существу представляет собой структурированную программу с идентифицированными циклами. Трудно делать это правильно; вам действительно нужна автоматическая процедура. Ваш бит кода, хотя и небольшой, по-прежнему довольно неприятен: -}
Я обманул в одном месте. Маурер настаивает на том, что вперед gotos в порядке, даже в середине цикла. Если вы купите это, вы сможете точно сохранить CFG. Если нет, вы должны обрабатывать случай, когда цикл имеет две или более точек входа; ваш алгоритм имеет такой цикл. Я решил проблему, закодировав цикл и закодировав эквивалент фрагмента loop-end-end-fragment, который действует как первая итерация перехода в середину, за которой следует сам цикл.
Мои обозначения немного забавны: у большинства языков нет "block {...}" конструкций. [Тот, который я кодирую (см. Биографию)]. Подумайте об этом как о цикле "выполнить одну итерацию": -} Я предполагаю, что блоки/петли имеют выходы цикла и цикл продолжается. Если у вас их нет, вы можете имитировать их с достаточным количеством только блоков {...} и exit_block @N.
ИЗМЕНИТЬ после принятия: в свете дня я не сделал это правильно, я ушел из цикла while @3. Я исправил это; необходимость в блочной конструкции теперь уходит, потому что я могу выйти из цикла while 3, чтобы добиться такого же эффекта. На самом деле код читается немного лучше.
Я оставил ваши числовые ярлыки, даже там, где они не нужны, для упрощения ссылки.
//Algorithm X;
1:
initialize();
2:
while (true) {
enter_level(k);
3:
while (true) {
set(a[k],q);
if (test() == ok) {
if (k != n) [email protected];
visit();
decrease(k); // replicate logic at 6 to avoid jumping into middle of 5 loop
if (k==0) return;
set(p,u_k);
}
5:
while (true) {
increasev2(a[k]);
if (q != 0) [email protected];
6:
decrease(k);
if (k==0) return;
set(p,u_k);
} // while(true)@5
} // while(true)@3
4:
increase(k);
} // while(true)@2
В отличие от большинства других ответов, которые я видел до сих пор, это выполняется с той же скоростью, что и оригинал (никаких дополнительных флажков или флагов).
@hatchet ответ интересен; а) он одинаково быстро, б) он решил обработать два петли ввода тем же методом, но он выбрал "другую запись" как верх петли. Он сделал что-то подобное с "enter_level (k)" на метке 2.
Интересно, что все это структурирование, похоже, не помогает читабельности этого кода один бит. Заставляет задуматься о цели "структурированных программ". Возможно, хорошо продуманные спагетти не так уж плохи: -}
Ответ 2
Без слишком больших усилий (и не много риска) вы можете быстро сократить количество gotos и меток.
1) удалять ярлыки, на которые не ссылаются нигде (это будет метка 1:)
2) найдите блоки кода, которые не могут быть введены, кроме goto, которые вызываются в нескольких местах. Они часто могут быть учтены просто. 4: можно решить, переместив код туда, где он звонил, и безопасно, так как его единственным выходом является goto. Это также позволяет нам удалить goto 5 над ним, так как этот код просто провалится до 5:. 7: можно обрабатывать, изменяя оператор if. В этот момент мы имеем
initialize();
2:
enter_level(k);
3:
set(a[k],q);
if(test() == ok) {
if (k == n) {
visit();
goto 6;
}
increase(k);
goto 2;
}
5:
increasev2(a[k]);
if (q != 0) {
goto 3;
}
6:
decrease(k);
if (k!=0) {
set(p,u_k);
goto 5;
}
return;
Я был бы склонен остановиться здесь. Но если вы продолжите, это станет вопросом идентификации циклов и замены gotos на конструкции цикла. Однако из-за того, как структурирован код, риск внесения этих изменений намного больше. Кроме того, вы, вероятно, закончите с перерывами и продолжениями, которые в любом случае являются готами. Я закончил с этим (и я не стал бы ручаться за его правильность без строгого строгого):
initialize();
enter_level(k);
while (true) {
set(a[k],q);
if(test() == ok) {
if (k == n) {
visit();
} else {
increase(k);
enter_level(k);
continue;
}
} else {
increasev2(a[k]);
if (q != 0) {
continue;
}
}
while (true) {
decrease(k);
if (k!=0) {
set(p,u_k);
increasev2(a[k]);
if (q != 0) {
break;
}
} else {
return;
}
}
}
Я сделал 3: петлю и 6: внутренний цикл. Я избавился от goto 5, скопировав код 5: вместо goto и заменив goto 3 на перерыв. Это облегчает создание чистых петель. Goo 6 фиксируется с использованием другого. Продолжение goto 3 продолжается.
После этого (если у вас есть энергия слева), вы можете попытаться изменить петли с (true) с продолжением в действительные условия.
Рекомендуется сначала разработать ваши тесты, а затем сделать одно или два изменения и проверить. Сделайте другое изменение, затем повторите тест. Если вы этого не сделаете, легко сделать структурную ошибку на ранней стадии, а затем аннулировать последующие шаги и заставить вас начать все заново.
Ответ 3
В С++ алгоритм может быть записан как:
void initialize() {}
void enter_level(int k) {}
void set(int x,int y) {}
bool test() { return true; }
void visit() {}
void increase(int k) {}
void increasev2(int k) {}
void decrease(int k) {}
void algorithm_x()
{
int k{0};
int a[] ={1,2,3,4,5};
int q{0};
bool ok{true};
int n{0};
int p{0};
int u_k{0};
//Algorithm X;
lbl1:
initialize();
lbl2:
enter_level(k);
lbl3:
set(a[k],q);
if (test() == ok) {
if (k == n) {
visit();
goto lbl6;
}
goto lbl4;
}
goto lbl5;
lbl4:
increase(k);
goto lbl2;
lbl5:
increasev2(a[k]);
if (q != 0) {
goto lbl3;
}
lbl6:
decrease(k);
if (k==0) {
goto lbl7;
}
set(p,u_k);
goto lbl5;
lbl7:
return;
}
int main()
{
algorithm_x();
return 0;
}
Предполагая, что мы не используем операторы break, программа могла бы быть следующей:
void initialize() {}
void enter_level(int k) {}
void set(int x,int y) {}
bool test() { return true; }
void visit() {}
void increase(int k) {}
void increasev2(int k) {}
void decrease(int k) {}
void algorithm_x()
{
int k{0};
int a[] ={1,2,3,4,5};
int q{0};
bool ok{true};
int n{0};
int p{0};
int u_k{0};
bool skiptail{false};
//Algorithm X;
initialize();
enter_level(k);
while (true) {
skiptail = false;
set(a[k],q);
if (test() == ok) {
if (k == n) {
visit();
decrease(k);
if (k==0) {
return;
}
set(p,u_k);
while (true) {
increasev2(a[k]);
if (q != 0) {
//goto lbl3;
skiptail = true;
}
if (!skiptail) decrease(k);
if (!skiptail) if (k==0) {
return;
}
if (!skiptail) set(p,u_k);
}
}
if (!skiptail) increase(k);
if (!skiptail) enter_level(k);
//goto lbl3;
skiptail = true;
}
if (!skiptail) while (true) {
increasev2(a[k]);
if (q != 0) {
//goto lbl3;
skiptail = true;
}
if (!skiptail) decrease(k);
if (!skiptail) if (k==0) {
return;
}
if (!skiptail) set(p,u_k);
}
if (!skiptail) increase(k);
if (!skiptail) enter_level(k);
//goto lbl3;
skiptail = true;
if (!skiptail) while (true) {
increasev2(a[k]);
if (q != 0) {
//goto lbl3;
skiptail = true;
}
if (!skiptail) decrease(k);
if (!skiptail) if (k==0) {
return;
}
if (!skiptail) set(p,u_k);
}
}
}
int main()
{
algorithm_x();
return 0;
}
В результате изменения использовался следующий алгоритм:
-
Избавьтесь от неиспользуемых меток. Удалите lbl1
-
Если ярлык заканчивается goto, замените этот блок, где он используется.
Удалите lbl4
, lbl6
, lbl7
-
если метка вернется к себе, тогда поместите блок во время (true).
Удалить снизу lbl5
(lbl5
теперь автономно и может быть заменен там, где он используется)
-
если блок самодостаточен, а затем замените его везде, где он используется.
Удалить lbl5
-
Если одна метка следует за другой, тогда поместите следующую метку в конец блока, чтобы ее можно было заменить в соответствии с правилом 2.
Удалите lbl2
(который может goto lbl3
)
-
теперь мы остаемся с последней меткой goto
по всему коду. Замените goto lbl3
на skiptail=true
, поместите оставшийся блок в блок while (true)
и установите остальные инструкции, чтобы проверить, есть ли skiptail=false
.
Удалите lbl3
и замените на skiptail = false
.
Ответ 4
Я никогда не использовал goto
, но это казалось забавным вызовом, поэтому я попытался самостоятельно реорганизовать.
Прежде всего, просмотрите код и посмотрите, сколько операторов goto
для каждой метки; это будет важно иметь в виду, чтобы избежать ошибок. В вашем примере ничего не приводит к 1, поэтому мы можем игнорировать его.
Иногда мне было удобно добавлять goto
, когда они подразумевались потоком управления. Это помогло отслеживать порядок, когда я переместил код между вещами.
Лучший способ реорганизации goto
работает вверх изнутри или снизу вверх.
-
Последняя команда 7:return;
, которую можно просто перемещать туда, где вызывается goto 7
. Это легко.
-
Затем я пытаюсь увидеть, какие метки заканчиваются на goto
(безоговорочно) и поступают сразу после другого goto. В этом случае это будет 4; он может быть перемещен перед 2, внутри, если контролируется дозорным (при подготовке к петле). (goto
мой первый пункт, чтобы увидеть, что 2 можно удалить сейчас.)
-
Следующее, что я сделал, было помещено 5 и 6 в цикл. Если бы я был неправ, я мог бы просто отступить в любом случае.
-
В этот момент я вижу, что 6 будет выполняться после 3 или 5. Я также вижу, что 5 может выполнить 3, поэтому я решил переместить 3 после 5. Я добавляю переменную, чтобы я мог пропустить 5 в первый раз. Я установил его в true в конце 6.
-
Чтобы обеспечить 5, можно перейти непосредственно к 6, когда это необходимо, я могу обернуть 3 в инструкции if с противоположным условием выполнения 5. Когда мне нужно перейти от 5 до 3, я могу изменить условие, пока я внутри 5, так что 3 выполняется сразу после этого.
-
В этот момент у меня есть только один goto
, который идет от 3 до 4. Если я изменил это на break
, я могу выйти из одного цикла, и я дойду до конца. Чтобы добраться до 4, я просто завершаю все (кроме 1) в цикле.
Вы можете использовать этот трюк, чтобы вырваться из вложенных циклов, не используя goto
, если у вас есть что-то из этого, но это не было необходимо в этом случае.
В конце концов, я получил этот код (ярлыки включены только для ясности):
1: initialize();
reached4=false;
do5 = false;
while(true){
if (reached4){
4: increase(k);
}
2: enter_level(k);
while(true){
if(do5){
5:
increasev2(a[k]);
if (q != 0) {
do5 = false;//goto 3
}
}
if(!do5){
3:
set(a[k],q);
if(test() == ok) {
if (k == n) {
visit();//goto 6;
}else{
reached4 = true;
break;//goto 4
}
}
}
6:
decrease(k);
if (k==0) {
7: return;
}
set(p,u_k);
do5 = true;
}
}
Ответ 5
Вы можете использовать множество переменных, чтобы имитировать поток gotos для работы с if's
и while's
initialize();
enterLevel = true;
executeWhile = true;
do
{
if (enterLevel)
{
enter_level(k);
}
enterLevel = false;
goto4 = false;
goto5 = false;
goto6 = false;
set(a[k],q);
if(test() == ok)
{
if (k == n)
{
visit();
goto6 = true;
}
else
{
goto4 = true;
}
}
else
{
goto5 = true;
}
if (goto4)
{
increase(k);
enterLevel = true;
}
else
{
do
{
if(goto5)
{
increasev2(a[k]);
goto6 = goto5 = !(q != 0); // if (q != 0) { goto6 = goto5 = false; } else { goto6 = goto5 = true; }
}
if(goto6)
{
decrease(k);
executeWhile = !(k==0); // if (k == 0) { executeWhile = false; } else { executeWhile = true; }
set(p,u_k);
goto5 = true;
}
} while (goto5 && executeWhile);
}
} while (executeWhile);
Является ли эта версия лучше, чем с goto's
, я не могу сказать.
Во-первых, я полностью разделяю все метки.
Затем я определил, что здесь находятся 2 цикла:
1 -
* label 4 -> goto 2
* label 5 -> goto 3.
Оба идут в начало кода, но один выполняет enter_level(k)
, а другой - нет. Вот почему параметр enterLevel var.
2 -
* label 6 -> goto 5. This goes up a little in the code, and then executes again.
Внутри этого цикла есть две ситуации, в которых он выходит:
* label 5 -> goto 3. The same as before, but now inside a nested loop
* label 6 -> goto 7. The way out of the outer loop.
Другие переменные и if предназначены только для управления потоком управления.
Да, я мог бы использовать некоторые перерывы (и код мог бы стать короче),
но поскольку вопрос касается goto's, я лично предпочитал не использовать их.