WIP: PyGOTO
parent
dadddffdea
commit
1c0c3eaee9
@ -0,0 +1,217 @@
|
||||
|
||||
Goto in Python
|
||||
==============
|
||||
|
||||
I really know that Python was meant to be used without a goto, but sometimes it
|
||||
really makes sense to use it.
|
||||
|
||||
## Goto usage patterns:
|
||||
|
||||
Let's first show a few common and in my opinion sensible (at least sometimes)
|
||||
uses of goto. Also, I'll discuss possible goto-free alternatives of the code
|
||||
and why they feel worse.
|
||||
|
||||
### Error handling
|
||||
|
||||
The most common one, used all over the Linux kernel for example:
|
||||
|
||||
```c
|
||||
// ...
|
||||
|
||||
if(! something_we_dont_want_to_fail()):
|
||||
goto err;
|
||||
// We know it hasn't failed, so we can use it
|
||||
|
||||
// ...
|
||||
|
||||
return result; // OK
|
||||
|
||||
err:
|
||||
clean_up()
|
||||
return 0; // This did not work.
|
||||
```
|
||||
|
||||
Often, there are more goto's for one label, and fall through is utilized, e.g.:
|
||||
|
||||
```c
|
||||
// Open the file
|
||||
if (file_empty(f))
|
||||
goto clean_files;
|
||||
// Read the file
|
||||
if (bad_data(f))
|
||||
goto clean_files;
|
||||
// Load data into memory
|
||||
if (could_not_process(data))
|
||||
goto clean_memory;
|
||||
return processed_data;
|
||||
|
||||
clean_memory:
|
||||
free(data);
|
||||
clean_files:
|
||||
close(f);
|
||||
|
||||
return 0;
|
||||
|
||||
```
|
||||
|
||||
The goto-free alternative is to put everything into functions and check all the
|
||||
functions. This might lead to creating functions with no semantics, and that
|
||||
does not generally make the code more readable.
|
||||
|
||||
The lucky part is, this is the case that is simple to solve with exceptions in
|
||||
much the same way.
|
||||
|
||||
There is still a case to be made with the multiple cleanup blocks and
|
||||
fall-through, but one of the ways of solving that is to have the exception know
|
||||
what to clean up. That might be a bit ugly, but the goto is way worse.
|
||||
|
||||
|
||||
### Alternative methods of evaluation
|
||||
|
||||
Sometimes, one would like to try several different paths to get a result, in
|
||||
some order, i.e. with pseudocode like this:
|
||||
|
||||
```c
|
||||
int result = 0;
|
||||
|
||||
// Method 1 (the most likely to work)
|
||||
result = method_1();
|
||||
if(result)
|
||||
goto done;
|
||||
|
||||
// Method 1 did not work, try method 2:
|
||||
result = method_2();
|
||||
if(result)
|
||||
goto done;
|
||||
|
||||
// Try method 3 maybe?
|
||||
result = method_3();
|
||||
if (! result)
|
||||
return ERROR;
|
||||
|
||||
done:
|
||||
// We now have result, so process that
|
||||
// ...
|
||||
```
|
||||
|
||||
Possible options to fixing this:
|
||||
|
||||
- Create a function that gets the result. In that function, the goto's can be
|
||||
replaced by return's, so that is nice, but the function might not have
|
||||
sensible name and need an unreasonable number of arguments, if the methods
|
||||
are complicated enough.
|
||||
- Reorder the methods to try from the least preffered to most. That obscures
|
||||
the algorithm, and might not be an option, if the methods have side effects.
|
||||
|
||||
There exists a solution with exceptions: Raise an exception when one method
|
||||
fails, and try other methods in the exception handling block. That leads to
|
||||
complicated catch[^I will call it "catch" block even though in Python this
|
||||
technically is an "except" block, because it seems more obvious what I mean.]
|
||||
blocks, which is not a good practice. And what is worse, for the third method
|
||||
the blocks would be nested!
|
||||
|
||||
### Retries
|
||||
|
||||
Sometimes, you need to try something several times, or skip something. In some
|
||||
code (not originally in C), I had something along the lines of:
|
||||
|
||||
```c
|
||||
struct line_with_number get_next_reasonable_line():
|
||||
// Imagine here a static FILE object, from which I read the lines.
|
||||
int linelen = 255;
|
||||
char *line;
|
||||
static int line_number
|
||||
|
||||
next_line:
|
||||
getline(line, linelen, file);
|
||||
if (line == NULL)
|
||||
// Just return some kind of error
|
||||
return {.line = NULL, .number = line_number};
|
||||
|
||||
line = strip_comments(line);
|
||||
if (strlen(line) == 0)
|
||||
goto next_line;
|
||||
// Look at the line more closely
|
||||
if (we_do_not_like(line))
|
||||
goto next_line;
|
||||
|
||||
return {.line = line, .number = line_number};
|
||||
```
|
||||
|
||||
The "obvious" solution to this is to use `while` and `continue`. But that is
|
||||
not necessarily better -- the code is quite likely to pass only once, so there
|
||||
is no semantics of a cycle. Also, what would be the condition here? We have no
|
||||
data, so maybe `while(true)`. Or in the better case, `do ... while(strlen(line)
|
||||
== 0 || we_do_not_like(line))`. When the condition is not so simple, using
|
||||
infinite loop is the way to go. But such code is not prettier, since it uses
|
||||
unreasonable flow control and loses the jump's meaning.
|
||||
|
||||
|
||||
There may be a few more places where a goto is a sensible option, but I don't
|
||||
recall any of them, so they might not be as important.
|
||||
|
||||
## What to do in Python?
|
||||
|
||||
There is no simple way around the second use-case for a goto. In some code, I
|
||||
ended up with following code (and to make it worse, that was inside a function,
|
||||
all of it):
|
||||
|
||||
```python
|
||||
# NOTE: This is really ugly goto, simulated using exceptions
|
||||
class EverythingIsOK(Exception): pass
|
||||
|
||||
try:
|
||||
# Method 1:
|
||||
result = method_1()
|
||||
if result: raise EverythingIsOK() # Goto
|
||||
|
||||
# Method 1 did not work, try method 2:
|
||||
result = method_2()
|
||||
if result: raise EverythingIsOK()
|
||||
|
||||
# Try method 3 maybe?
|
||||
result = method_3()
|
||||
if not result:
|
||||
raise OperationalError() # Not OK, Not goto.
|
||||
except EverythingIsOK: # Label
|
||||
pass
|
||||
|
||||
# Process result
|
||||
```
|
||||
|
||||
This is ugly enough, but can only jump downwards. Jumping upwards probably
|
||||
needs the trick with `continue`, and unfortunately, there is no `do-while` in
|
||||
Python.
|
||||
|
||||
```python
|
||||
while True: # Label
|
||||
# code
|
||||
continue # Goto
|
||||
# code
|
||||
break # Boilerplate
|
||||
```
|
||||
|
||||
If you do not like that the goto's look different, you can use this mess:
|
||||
|
||||
```python
|
||||
class MyGoto(Exception): pass
|
||||
while True: # Label
|
||||
try:
|
||||
# code
|
||||
raise MyGoto() # Goto
|
||||
# code
|
||||
break
|
||||
except MyGoto: # Even more boilerplate.
|
||||
continue
|
||||
```
|
||||
|
||||
I have no solution for using a single label for jumping from above _and_ below.
|
||||
But I hope I will never see code that needs that.
|
||||
|
||||
This post is not meant to encourage use of goto. It is considered harmful, and
|
||||
in most cases, rightfully so. But still, there are some reasonable use cases,
|
||||
so it is worth knowing when to use it and what can you do when the language
|
||||
does not support goto.
|
||||
|
||||
And probably needless to say: I would still rather have native goto support,
|
||||
than need to use awful hacks to simulate the flow which I have in mind.
|
Loading…
Reference in New Issue