6. File Input and Output for the Game of Life

The lifeio module defines file input-output operations for the Game of Life. It exemplifies CLEAN's file I/O facilities defined in the library module deltaFile. A file object, of type FILE, is actually a file descriptor which contains a pointer into a physical disk file.

IMPORT deltaFile,lifepoint;

PSsquare -> 450;

The FWriteL function recursively writes a list of strings to a file, using the FWriteS primitive function to write a string. The function defined here is written in a functional style, with f referring to the initial file, and f' referring to the file after writing one string. However, the output to the file is actually performed as an update in place, in the usual way. This is possible using CLEAN's unique type mechanism.

An object of type UNQ T (such as f) cannot be shared, and an attempt to copy the object is detected as a type error. Since the object is used only once, it can be safely updated in place. The type of FWriteS is !STRING => !UNQ FILE => UNQ FILE (in infix notation), so that the result f' is also of unique type. The use of functions of type UNQ FILE => UNQ FILE essentially forces access to the file to be single-threaded. The strictness annotation on the file argument forces evaluation of file writing operations in the argument.

Unique types are closely related to linear types. The unique type mechanism offers an elegant handling of file update in the functional context, and could also be used to handle array operations with update in place.

   FWriteL [] f -> f;
   FWriteL [h|t] f -> FWriteL t f',
                      f': FWriteS h f; 

Since the predefined file closing operation FClose in the current version of CLEAN will not operate on the standard files StdIO and StdErr, we provide a function with the same type to take its place. This function forces evaluation of all input and output operations on the file, but does not allow the file to be re-opened (and so we return a false success status). In CLEAN, opening and closing a file is treated conceptually as taking a file out of, or placing it back into, a file system of type FILES.

:: FCloseStd !UNQ FILE !FILES -> (!BOOL, !FILES);      
   FCloseStd f g -> (FALSE, g);

The SReadPtsList function reads a list of points from a (possibly shared) text file, using non-unique shared-file operations. Such operations can only read, but not modify a file. The recursive loop terminates on end of file, or when an attempt to read an integer with SFReadI fails. Notice the sequence of file values: f is the initial file, f' is the file after having read one integer, and f'' is the file after reading a second integer (i.e. f'' contains a pointer into the file which points just after the second integer). It is this final file value which is passed to the recursive call.

A shared file object can be copied many times, and each copy can be used as the basis for read operations. Consequently, when a physical file is opened as a non-unique file (using SFOpen) it cannot be closed, since there may be many file objects pointing into the physical file.

:: SReadPtsList !FILE -> PntList;
   SReadPtsList f -> [], IF SFEnd f || NOT status1 || NOT status2
                  -> [(x,y) | SReadPtsList f''],
                     (status1, x, f'): SFReadI f,
                     (status2, y, f''): SFReadI f';

The ReadPtsList function is like SReadPtsList, but reads from an unshared text file, using unique-file operations. These require single-threaded use of the file, which complicates the function definition. However, single-threaded operations allow the final file object to be returned for later closing. Since unique files cannot be copied, they can be safely closed.

To ensure single-threaded use, only shared-file operations can be applied to a file in guards. This is because it cannot be decided if a guard will be executed, and also because a guard returns only a boolean result, and cannot return a file object. Thus the test for end of file must be done using the shared-file operation SFEnd.

:: ReadPtsList !UNQ FILE -> (!PntList, !UNQ FILE);
   ReadPtsList f -> ([], f), IF SFEnd f
                 -> ([], f'), IF NOT status1
                 -> ([], f''), IF NOT status2
                 -> ([(x,y)|rest], f'''),
                    (status1, x, f'): FReadI f,
                    (status2, y, f''): FReadI f',
                    (rest,f'''): ReadPtsList f'';

The WritePtsList function writes a list of points to a unique text file. The sequence of operations is to write x to f, then write a space, giving f', and then write y and a newline, giving f'' which is passed to the recursive call.

:: WritePtsList !PntList !UNQ FILE -> UNQ FILE;
   WritePtsList [] f -> f;
   WritePtsList [(x,y)|t] f -> WritePtsList t f'',
                               f': FWriteC ' ' (FWriteI x f),
                               f'': FWriteC '\n' (FWriteI y f');

The WritePS function is more sophisticated, writing an Encapsulated PostScript picture of a list of points occupying a given rectangle, with a string to print as heading. It uses three auxiliary functions, after calculating the radius of the circles used to indicate live cells. An example Encapsulated PostScript file generated by this function was imported directly into this document as Figure 1. The PSsquare macro defined at the start of the module indicates the size (in PostScript units) of the square in which the pattern is drawn.

:: WritePS !STRING !Rect !PntList !UNQ FILE -> UNQ FILE;
   WritePS s ((a,b),(c,d)) l f 
        -> f''',
           f': PSheader s radius f,
           f'': PSPtsList xoffset yoffset a b size l f',
           f''': PStrailer f'',
           width: + 2 (- c a),
           height: + 2 (- d b),
           size: Max 2 (Min (/ PSsquare height) (/ PSsquare width)),
           radius: Max 1 (/ (* 8 size) 20),
           actualwidth: * size (- c a),
           actualheight: * size (- d b),
           xoffset: / (- PSsquare actualwidth) 2,
           zoffset: / (- PSsquare actualheight) 2,
           yoffset: - PSsquare zoffset;

The first auxiliary function uses FWriteL to write a list of strings to form an Encapsulated PostScript header. This provides a convenient way of performing formatted output. The ITOS primitive function converts an integer to a string.

   PSheader s radius f
    -> FWriteL ["%!PS-Adobe-2.0 EPSF-2.0\n",
                "%%Title: ", s, "\n",
                "%%Creator: LIFE package by A. H. Dekker\n",
                "%%BoundingBox: 19 99 ",
                ITOS (+ 21 PSsquare), " ",
                ITOS (+ 171 PSsquare), "\n",
                "%%DocumentFonts: Times-Bold\n",
                "/origstate save def\n",
                "20 dict begin\n",
                "20 100 translate\n\n",
                "0 ", ITOS (+ 50 PSsquare), " moveto\n",
                "/Times-Bold findfont 20 scalefont setfont\n",
                "(", s, ") show\n",
                "1 setlinewidth 0 0 moveto\n",
                ITOS PSsquare, " 0 rlineto 0 ",
                ITOS PSsquare, " rlineto ",
                ITOS (- 0 PSsquare), " 0 rlineto closepath stroke\n",
                "/dot {", ITOS radius, " 0 360 arc fill} bind def\n",
                "\n"] f;

The second auxiliary function recursively writes a list of points in a similar way to WritePtsList:

   PSPtsList xoff yoff a b size [] f -> f;
   PSPtsList xoff yoff a b size [(x,y)|t] f 
        -> PSPtsList xoff yoff a b size t f'',
           f': FWriteC ' ' (FWriteI (+ xoff (* size (- x a))) f),
           f'': FWriteS " dot\n" (FWriteI (- yoff (* size (- y b))) f');

The final auxiliary function writes an Encapsulated PostScript trailer:

:: PStrailer !UNQ FILE -> UNQ FILE;
   PStrailer f
    -> FWriteL ["\n",
                "origstate restore\n"] f;

The lifeio definition module (lifeio.dcl) lists the five exported functions of the module.

UpTop Level

BackA Simple CLEAN Program


ForwardA File Conversion Program