📜 ⬆️ ⬇️

Goto in application programming


Picture from book Thinking Forth

Based on the “generalized” article about GOTO and the article about GOTO in system programming :

The motives for using GOTO and alternatives are fundamentally different for system and application programming - this is also an important reason for holivars. To clarify the situation, consider GOTO only in the context of application programming.
')
The main thesis : in application programming, GOTO is definitely better to bypass.

How to prove :
  1. In application programming, one parameter of the code is critical - maintainability.
  2. Goto does not worsen uniquely maintainability only in a small percentage of cases, and even in these cases it does not differ fundamentally from alternatives.
  3. For the sake of a small percentage of cases, it is harmful to use it:
    1) very low-level, therefore, it greatly corrupts the programmer (it is tempting to use in other places) - a great deal of harm due to the small percentage of cases when GOTO can be applied;
    2) even in such cases there are more beautiful alternatives.



GOTO - properties and impact on code quality


Code Quality Parameters
(we believe that the code is written correctly, all non-standard situations are taken into account):
  1. Resource consumption (memory, processor cycles) - priority "from the machine"
  2. Maintainability (ease of maintenance) of the code - the priority "from the person":
    1) readability of the code - how easy it is to understand the written code (respectively, how easy it is to write and debug),
    2) robustness in case of changes - how difficult is it to introduce an error / how easy is it to notice when changing the code,
    3) the ease of maintaining a common standard - how much code writing teaches the programmer to deviations from some common standards
General properties of GOTO:
  1. unstructured : it is possible to insert in an almost arbitrary place, it is difficult to understand how we got there, unlike other branch constructions;
  2. enslaves the source code : if structured blocks can be changed in different ways, rearranged their order, as in the constructor, then GOTO is the nail connecting some constructor blocks - after its implementation the code is already very difficult to change.

Using GOTO affects both the code quality parameters and their components. It is clear that the consumption of GOTO resources reduces by a small constant into which it is embedded, since it never changes the complexity of the algorithm.

What is not GOTO:
  1. Other flow control constructions - if, switch, while, etc.: in them all branches of the flow are rigidly defined by the syntax, are on the border of the same block - the upper or lower (for return - the border of the function), and GOTO can be placed arbitrarily
  2. automatically generated code - as in the generated assembler, you don’t have to dig into it and maintain it.

GOTO features in application programming


Application programming here is programming in high-level languages ​​that support code structuring, including a structured approach to exception handling: Java, C #, C ++, interpreted languages, etc. - in general, the standard applied mainstream. I do not consider C as a low-level language, which is currently used mainly for system programming.

Features of application programming:
  1. there is no need for point optimizations - individual cycles or memory cells, therefore saving of resources can be discarded from consideration of GOTO - only maintainability remains
  2. There is an opportunity to structure logic in any way - somehow combine into functions / methods, set up as many variables, classes, etc. with any names, the ability to throw exceptions
GOTO only worsens code maintenance.

In system programming, the maximum saving of resources is important, so there, perhaps, the use of GOTO for this purpose is justified.
In application programming, the “resource consumption” parameter can be dropped, only the maintainability parameter remains, which GOTO degrades.


GOTO - problems and fixes


Consider the use of GOTO in various ways to move through the structure of the code and alternatives to it:

1. Entrance to the block from the outside:

1.1 Entry to "not a cycle":
easily and obviously rewritten without goto:

if(a) GOTO InsideIf; if(b) { foo(); InsideIf: bar(); } 

=>

  if(b) { foo(); } if(a || b) { bar(); } 

1.2 Entering the cycle:
it is impossible: generally the execution flow is not clear:

  if(goIntoCycle) GOTO MiddleOfCycle; for(...) { foo(); MiddleOfCycle: bar(); } 


2. Transition within one block:
no need, easy to rewrite, usually to if / else:

  if(bNotNeeded) GOTO startFromC: B(); startFromC: C(); 

=>

  if(bNeeded) { B(); } C(); 


3. Exit the unit to the outside

This is the main case of the possible use of GOTO. We divide it into even smaller ones and consider in detail using examples.
The general approach is to decompose as much as possible: we break into methods by meaning, we fix logic in flags with speaking names — we get readable and self-documented code.

Important rules:
  1. DO NOT USE EXCLUSIONS :

    1) we always use exceptions to handle errors and abnormal situations, so we don’t use them for anything else, so as not to put off an eye;

    2) we can accidentally “swallow” an exception from the internal level of nesting;

    3) it is an expensive pleasure - in applied programming it is worthless to crumb a little, but there is no point in scattering resources in the presence of simple alternatives;
  2. We avoid multiple returns , especially from different levels of nesting: otherwise there are few differences from GOTO - the readability of the code is also difficult in most cases. (Perhaps this requires a separate justification - also the one that is still holivar).

3.1. The only way out of one level of nesting:
is trivially replaced by if / break, etc.

3.2. Multiple exits from the same nesting level:

3.2.1 Error handling - only through exceptions
(I hope this is obvious; if not, I can explain it in a separate article)

3.2.2 Examining options - using the example of if:

  class Tamagochi { function recreate() { if(wannaEat) { eat(); GOTO Cleanup; } if (wannaDrink) { drink(); GOTO Cleanup; } if(wannaDance) { Dance(); } return HAPPY; //true Cleanup: return washHands() && cleanKitchen(); } } 

Problems (except for the inherent flow of execution that is always inherent in GOTO):
you wanted to add sleep behavior in the cases of wannaEat and wannaDance - everything, the closing for wannaEat and wannaDrink is destroyed.

How to make beautiful (immediately extended version):

  function recreate() { if(wannaEat) { eat(); needCleanup = true; needSleep = true; } if (wannaDrink) { drink(); needCleanup = true; } if(wannaDance) { Dance(); needSleep = true; } result = HAPPY; //true if(needCleanup) result = result && washHands() && cleanKitchen(); if(needSleep) result = result && sleep(); return result; } 


3.3. Exit multiple levels of nesting.

3.3.1 If it is easy to isolate different logic (different responsibilities):

  class BatchInputProcessor { function processInput(inputRecords) { for (inputRecord in inputRecords) { for (validator in validators) { if (!validator.check(inputRecord) GOTO ValidationFailed; } item = new Item(); for (fieldValue in inputRecord.fieldValues) { setFieldValue(fieldValue.field, fieldValue.value); } itemsToStore.add(item); } return store(itemsToStore); ValidationFailed: log(failedValidation); tryToCorrect(inputRecords); ... } } 

=>

  function processInput(inputRecords) { allRecordsAreValid = true; for(inputRecord in inputRecords) { recordIsValid = validateRecord(inputRecord, validators); if(!recordIsValid) { allRecordsAreValid = false; break; } else { itemToStore = createItemFromRecord(inputRecord); itemsToStore.add(item); } } if(allRecordsAreValid) { result = store(itemsToStore); } else { log(failedValidation); tryToCorrect(inputRecords); ... } } 


3.3.2 It is more difficult to isolate different logic or the code becomes more complicated.

As a rule, this can be the case for nested loops of the same type:

  function findOrCreateTriadaOfEqual(firstArray, secondArray, thirdArray) { result = null; for(first in firstArray) { for(second in secondArray) { for(third in thirdArray) { if(first == second && second == third) { result = array(first, second, third); GOTO Found: } } } } equal = new Item(); result = array(equal, equal, equal); Found: log(result); return result; } 


There are still a lot of options:
  1. to put in a separate subfunction with return from the inner loop - the simplest
  2. summarize - make the recursive function of the form findEqualInArrays (arrayOfArrays, currentArrayIndex, currentFoundItemsArray);
  3. “If (result) break” is the most clumsy:
  result = null; for(first in firstArray) { for(second in secondArray) { for(third in thirdArray) { if(first == second && second == third) { result = array(first, second, third); break; } } if(result) break; } if(result) break; } 


This is the only option that looks worse than GOTO, and GOTO is even clearer. But almost always there are other options.

For the remaining vanishingly small percentage of cases where there are no other options, you just need to decide what you can do at least with flags, but the guidelines will be simpler - “Without GOTO!”.

Summary:


Most importantly - maintainability .

GOTO always worsens maintainability , therefore

need to do without GOTO - enough standard tools:
  1. error handling through exceptions;
  2. decomposition - a large method that solves many problems, is divided into small, solving individual problems;
  3. fixing logic (computed conditions and expressions) in variables with talking names.

Source: https://habr.com/ru/post/114470/


All Articles