A common piece of code is:
- Create a temp file
- Use the temp file
- Delete the temp file
The straightforward implementation:
PROC doIt()
string tempFileName = createTempFile()
# write to the temp file, do some work
IO.delete(tempFileName)
}
The problem here is that "do some work" may throw an exception or crash the program.
Then the temp file is left behind. How do we make sure this doesn't happen?
Using try/finally:
PROC doIt()
string tempFileName = createTempFile()
TRY
# write to the temp file, do some work
FINALLY
IO.delete(tempFileName)
}
}
This is a working solution. But it gets more complicated when there is more than one resource to keep track of, for example when reading from one file, using the temp file, then writing to an output file.
Complicated try/finally:
PROC doIt()
IO.File in = IO.fileReader(inputFileName)
TRY
string tempFileName = createTempFile()
TRY
# read from "in", write to the temp file, do some work
IO.File out = IO.fileWriter(outputFileName)
TRY
# write to "out"
FINALLY
out.close()
}
FINALLY
IO.delete(tempFileName)
}
FINALLY
in.close()
}
}
This works, but by now it's hard to see the code that does the work in between all the exception handling.
Using Finish()
Another solution is to use object destructors. The Finish method will be invoked once an object is no longer in use
and eventually the garbage collector will destroy it. Or the latest when the program exits.
The IO.File class already defines a Finish method that closes the file, we don't need to do anything for that.
We do need to define a Finish method for our temp file:
CLASS TempFile
string $fileName
NEW(string fileName)
$fileName = fileName
}
FUNC $Finish() status
IO.delete($fileName)
RETURN OK
}
}
PROC doIt()
IO.File in = IO.fileReader(inputFileName)
string tempFileName = createTempFile()
TempFile tf = NEW(tempFileName)
# read from "in", write to the temp file, do some work
IO.File out = IO.fileWriter(outputFileName)
# write to "out"
}
Notice that all the close() calls have been omitted. Although that's OK, it does mean that the file handle is in use until the garbage collector runs.
We might run out of file handles when doing this very often. We could use put the close() calls back, the Finish() method will become a no-op.
Another way is to put the objects on the stack. Then the destructor will be called the moment the function ends.
Finish() with objects on the stack
Prepending % before a local variable name puts it on the stack, instead of allocating it on the heap.
This is possible so long as we only assign to it from NEW() and never use the object once the method returns.
CLASS TempFile
# as above
}
PROC doIt()
IO.File %in = IO.fileReader(inputFileName)
string tempFileName = createTempFile()
TempFile %tf = NEW(tempFileName)
# read from "in", write to the temp file, do some work
IO.File %out = IO.fileWriter(outputFileName)
# write to "out"
} # %in, %tf and %out are destructed here
This is a solution that is easy to understand, is efficient and avoids making mistakes. It does require adding the TempFile class.
Using DEFER
There is one more thing we can do to make this simpler: Instead of creating a class with Finish() we can use DEFER:
PROC doIt()
IO.File %in = IO.fileReader(inputFileName)
string tempFileName = createTempFile()
DEFER IO.delete(tempFileName)
# read from "in", write to the temp file, do some work
IO.File %out = IO.fileWriter(outputFileName)
# write to "out"
} # %in and %out are destructed here, IO.delete() called
The DEFER statement takes a method call as an argument. That method is then called at the end of the current method.
Also when an exception would be thrown. And also without putting an object on the stack.
A limitation of DEFER is that it can only do a method call. The arguments are evaluated at the DEFER statement.
If you need something more complicated it's probably best going back to the Finish() method.
Although it's also possible to define a method at the DEFER with a lambda method. Only do that if that doesn't get too complicated.