I’m exploring Ada and ran into something that surprised me. I wrote this simple program:
with Ada.Text_IO; use Ada.Text_IO;
procedure Test is
File : File_Type;
Line : String(1..100);
Last : Natural;
begin
Open(File, In_File, "test.txt");
Get_Line(File, Line, Last);
-- oops, forgot Close(File)
Put_Line(Line(1..Last));
end Test;
It compiles fine and runs fine, but I never closed the file. I know Ada is actually pretty typical here (compared to other languages), but Ada is “sold” as this “safe” language, so I’m a little bit surprised.
What happens in bigger programs? Have faith in programmer discipline?
Is there some Ada wisdom I’m missing here? Do you just get good at remembering to close everything (just like in C/C++), or are there tricks/tools that help catch this resource leak?
I’m new to the language and still learning, so please, be patient with me
All files are closed on program exit, so there is no particular safety issue here. There is the possibility of resource exhaustion, if you keep opening files and never close them.
You could wrap your file handle in a “controlled” type if you want to get control when the file handle goes away, so you could explicitly call “Close” on it.
There’s really three pieces of wisdom that apply, though they’re general:
Remember Scoping
Ada is very good about being able to nest constructs; you can, for example, put a generic inside a package inside a declare-block inside a function — this allows for you to use scoping to help manage your complexity; and memory as well.
Use Ada’s Type-system to Model Your Problem
One thing that will make your programs much nicer, is to use the language’s features to model your problem-space, particularly/especially using the type-system, and then use that to solve your problem. — Structuring this way very often pushes externalities to the edge: eg having all your I/O done with Streams, internally, so that your dependency for files is only the small[ish] subprogram opening-read/writing-closing the file; thus keeping the File-object “in mind” for the whole operation.
You Can Combine Scoping with the Type-system’s Finalized Object
IOW, you can “make the language do the work” by combining the above two points, and have the language take care of everything… it’s not automatic, but the language allows you to use Finalization to manage things appropriately.
In fact, I wrote up a library to handle everything — see EVIL.Util.Files in EVIL — it’s a generic package that allows you to program against its own interface, allowing you to seamlessly use [[Wide_]Wide_]Text_IO, and providing a wrapper-type that closes the file on finalization, as well as presents the Stream associated with the file.
ARM A.7(6) says, “The language does not define what happens to external files after the completion of the main program and all the library tasks (in particular, if corresponding files have not been closed).” However, ARM 10.1(86/2) says, “The type File_Type needs finalization”. It doesn’t say why it needs finalization, or what finalization should be performed, but an obvious finalization for files is closing them if they’re open.
If you’re worried about files being left open, you might want to use a library like Controlled I/O.