Computers: Universe: Basic File I/O

Opening a file: OPEN

Before you can read from or write to a UniVerse file, you must open it using the OPEN statement. This stores a kind of 'file handle' in a file variable, and you may then use the file variable in your READ and WRITE statements. An example of an OPEN statement runs:

OPEN 'VOC' TO VOC.FILE ELSE STOP "Sorry, can't open VOC."

Note the ELSE clause. Your OPEN statement must have an ELSE clause to tell it what to do if it can't open the file: for instance, if the file name you have provided does not exist. In this case, I've decided to simply stop the program if it cannot be found, but you must decide the best approach in each case.

Reading and writing records using dynamic arrays: READ and WRITE

To read a record from a file, you need a file variable for the file, and a key for the record. You can then store the contents of the record in a variable. An example follows:

READ VOC.RECORD FROM VOC.FILE, 'RELLEVEL' ELSE VOC.RECORD = ''
PRINT VOC.RECORD ; * Prints something like Xþ9.4.1.1þINFORMATION...

The contents of the RELLEVEL record from the VOC file are loaded into VOC.RECORD as a dynamic array: the PRINT statement lets you take a look at what you've got. (You may remember that RELLEVEL is a standard VOC record which contains UniVerse release level information: see Other VOC Records).

Note again the ELSE clause: this is executed if the record you are looking for does not exist. Again, I've chosen to simply use an empty string as the record value in this case, but it might be more appropriate for you to display an error message, assign some non-empty default value, or even offer to create the missing record: it depends, of course, on your application. Setting records to empty strings in ELSE clauses has become something of a knee-jerk coding reaction, and probably contributes to many obscure bugs in UniVerse applications, which would have been highlighted earlier if an ELSE clause somewhere actually reported a problem.

To write your record, use WRITE:

WRITE VOC.RECORD TO VOC.FILE, 'TEST.WRITE'

Here, I have written the record back under a different key, effectively creating a new record. Note that WRITE will create new records if the key is not already on the file, or overwrite an existing record if it is, without a murmur. If either condition would constitute an error in your program, you must check for it using READ before writing.

Reading and writing records using dimensioned arrays: ,MATREAD and MATWRITE

MATREAD and MATWRITE correspond exactly to READ and WRITE, except that instead of using a dynamic array as a record memory buffer, they use a dimensioned array. You must therefore declare a dimensioned array to act as a buffer:

DIM VOC.RECORD(10)
MATREAD VOC.RECORD FROM VOC.FILE, 'RELLEVEL' ELSE MAT VOC.RECORD = ''
FOR FIELD.NUMBER = 1 TO 10
   PRINT VOC.RECORD(FIELD.NUMBER)
NEXT FIELD.NUMBER

The VOC file is awkward to handle in this way because it's records contain different numbers of fields. The RELLEVEL record is only four lines long, but a paragraph could easily contain 50 lines.

MATREAD handles such variables as best it can. If the record contains fewer fields than there are elements in the array, the unused elements are assigned empty string values. If there are more fields than elements, the additional fields are put into the special element 0 as a dynamic array. This element can be accessed using the syntax VOC.RECORD(0) but its perhaps best not to rely on this kind of overflowing handling within your logic. Increase the size of your dimensioned array, or use a dynamic array, rather than habitually searching element 0. (Note that I am assuming here that you are using Prime INFORMATION or IDEAL flavoured UniVerse: the others store excess fields in the last element, but the same principles apply.)

You can write the dimensioned array back to the file using MATWRITE:

MATWRITE VOC.RECORD TO VOC.FILE, 'TEST.WRITE'

MATWRITE is as happy to overwrite existing records or create new ones as WRITE, so again, add whatever tests you need to to prevent behaviour which shouldn't be happening.

Reading a single field from a file: READV

Though it is far less commonly done, you can read and write individual fields rather than entire records. It generally isn't any faster than reading and writing entire records, but may be a neater implementation of your design. Fields are read using READV:

READV TYPE.DESC FROM VOC.FILE, 'RELLEVEL', 1 ELSE TYPE.DESC = ''

Note here the 1 after the key 'RELLEVEL'. This tells READV which field number you wish to read. As usual, READV takes an ELSE clause: but note that it will only be executed if the record you are looking for does not exist, not merely if the field number you have requested is greater than the number of fields in the record: in that case it simply reads an empty string.

Predictably, to write a single field to a record you use WRITEV:

WRITEV TYPE.DESC TO VOC.FILE, 'TEST.WRITE', 1

If the record you are writing to exists, only the field number you have specified will be overwritten: the others will retain their values. If it doesn't, it will be created, with sufficient field marks (and thus empty fields) to put the field you have written in at the appropriate position.

Deleting records: DELETE

To delete a record, use the DELETE statement:

DELETE VOC.FILE, 'RELLEVEL'

Locking records for update: READU, MATREADU and READVU

UniVerse, like most of its predecessors, is primarily designed for the creation of multiuser systems: and you should bear this in mind in coding your file handling. If you use READ to read a record, modify it's contents in memory, and then WRITE it back, what is to say that some other user hasn't READ the same record just before you and written it back just after you? If they had, all the changes you made would be wiped out by the other users WRITE.

To prevent this kind of problem, UniVerse provides alternatives to READ, MATREAD and READV called READU, MATREADU and READVU respectively. The 'U' stands for 'update': and if you are planning to update a record, you should read it with a ...U read.

The only difference this makes is that UniVerse will lock a record when you READU it, preventing any other user from using READU to read it. If you try to READU a locked record, what will happen will depend on your code. Consider the following READU.

READU VOC.RECORD FROM VOC.FILE, 'RELLEVEL' ELSE VOC.RECORD = ''

If RELLEVEL is already locked, this READU will simply wait until the lock is removed: effectively hanging your program. It will not take the ELSE clause just because the record is locked: only if the record doesn't exist..

Although simply waiting for the lock to become available may be a suitable strategy on occassion, sometimes you will need to take specific action on encountering a lock. To do this, add a LOCKED clause.

READU VOC.RECORD FROM VOC.FILE, 'RELLEVEL' LOCKED
   STOP 'Sorry, the record is locked by another user. Try again later.'
END ELSE
   VOC.RECORD = '' ; * RELLEVEL doesn't exist
END

The message in the LOCKED clause above is not strictly accurate, as the RELLEVEL record may not even exist: if you READU a key which doesn't exist, you still create a lock on that key, effectively reserving it for yourself. Anyone else trying to READU the same key will find it locked.

A locked record (or a locked new key) will remain locked until one of four things happen:

1. You write it: WRITE VOC.RECORD TO VOC.FILE, 'RELLEVEL'
2. You release it: RELEASE VOC.FILE, 'RELLEVEL'
3. You program finishes running.

Don't rely on the third route: some programs can keep records locked for long periods, causing serious disruption to other processes, and a program which loops (like a data entry screen looping through updates or a batch processing routine) can lock huge numbers of records. Also, what you write today as a 10 second utility to update 100 records may be extended by future programmers to update thousands of programs: a problem if it doesn't release it's record locks.

The RELEASE statement can also be used to release all the locks held by a user on a particular file (RELEASE VOC.FILE, with no record key) or all the locks held by a user on any file (just RELEASE), but if you find yourself using either, it may be a sign that your program is holding locks unecessarily long. At some point, your program must decide whether or not to WRITE the record. It is at that point that you should RELEASE the individual record if you have decided not to WRITE it.

Locking records for reading: READL, MATREADL and READVL

It might be that you do not intend to update a record you have read, but wish nevertheless to ensure that noone else updates it while you are using it. To do this, use READL, MATREADL or READVL. These apply 'shared locks': UniVerse will prevent a user placing an update lock with READU on a record which has one or more shared locks applied by READL, and will prevent a READL placing a shared lock on a record which already has a READU update lock, but will allow many users to simultaneously apply READL shared locks.

A Locking Strategy

Bear in mind that neither READL nor READU will prevent a READ. The UniVerse locking system is cooperative rather than preemptive: which means that it relies on processes using the locking system to control their own behaviour rather than forcing a locking regime on them which they cannot avoid. This system works well if applied consistently, but will obviously fail wherever a programmer decides to WRITE a record for which he has never obtained READU lock.

A comparison might be made with C memory handling. C provides all the functions which are needed to manage memory safely: provided that programmers are careful never to address memory that they haven't reserved or 'allocated', and provided that they release it one they've done with it. It doesn't, however, enforce good practice, and both through guile and error far too many C programs still write to memory they don't own.

To safely manage multiuser access to your UniVerse System:

1. Do not create, modify, or delete a record without first obtaining an update lock using READU (or equivalent).

2. Do not assume that a record has remain unchanged since you read it unless you originally obtained a READU or READL lock on it which you have not yet released.

Processing select lists

Select lists, and some of their uses, were covered in an earlier section of this course (see Select Lists). These notes now discuss how such lists can be created and used within a UniVerse Basic program.

One method involves introducing a new UniVerse Basic statement which has applications beyond the creation of select lists: EXECUTE. You may remember a similarly editor command called XEQ (see The Editor - ED) which allowed you to execute TCL commands without leaving the editor. The UniVerse Basic statement XEQ serves a similar purpose, allowing you to execute TCL commands from a program. The syntax is simple, as is shown in the example below:

PRINT 'Press RETURN to see a listing of the VOC file...':
INPUT PAUSE
EXECUTE 'LIST VOC'

Combining this command with a TCL SELECT command allows you to create your select list from your UniVerse Basic program, using any of SELECT's usual parameters.

EXECUTE 'SELECT VOC WITH TYPE = "M"'

The second way to create a select list from your program is to use the UniVerse Basic SELECT statement. This simply takes a file variable, created with OPEN, as a parameter. An example follows:

SELECT VOC.FILE

Please note the differences between these approaches:

1. The TCL SELECT command can be used from the UniVerse command prompt. It can use WITH and BY clauses to modify the scope and sorting of its list. It accepts a filename as a parameter. It can only be used from a UniVerse Basic program via the EXECUTE statement.
2. The UniVerse Basic SELECT statement can only be used in a UniVerse Basic program. It cannot apply conditions or sorting to the lists it creates. It accepts a file variable as a parameter.

From this description, it will seem that using EXECUTE 'SELECT...' is far more flexible than using SELECT: and so it is. But the list of differences is not complete: the most important follows:

3. Using the UniVerse Basic SELECT command is much, much faster than using EXECUTE 'SELECT...'.

So, as always, it's up to you. If you cannot live without a sorted or scoped select list, you'll have to use EXECUTE 'SELECT filename parameters'. If you can, you'll be able to use the much faster SELECT filevariable.

Having created your select list, you must access the keys it contains from your program. To do this, UniVerse provides the READNEXT statement. By its nature, it is almost always written into a LOOP/REPEAT structure.

OPEN 'VOC' TO VOC.FILE ELSE STOP "Sorry, can't open VOC."
EOF = 0
SELECT VOC.FILE
LOOP
   READNEXT VOC.KEY ELSE EOF = 1
UNTIL EOF
   PRINT VOC.KEY
REPEAT

The READNEXT statement simply reads one key from the select list each time it is executed, until it runs out of keys and executes its ELSE clause. The logic above is fairly cautious: it's not uncommon to see the ELSE clause setting the key to an empty string in its ELSE clause, and then exiting as soon as it encounters an empty string. The problem is, though it is not easy, it is possible to get keys with empty string keys into UniVerse files: I once spent a long time debugging a program until I realise that READNEXT was returning an empty string as a key before it ran out of keys, thus terminating the loop early and leaving half the file unprocessed. Setting the key to an exotic value (like '*** END OF FILE ***') is also sometimes used, but there is always the possibility, however weird your special value, that a record in the file will have it: most probably because during a test run your loop didn't terminate properly and you ended up writing your special value to the file. Using an entirely separate flag is the only completely safe method.

Why is the UniVerse Basic SELECT statement so much faster than the equivalent TCL command? The reason is that the UniVerse Basic SELECT doesn't actually create a select list at all. It creates a kind of internal pointer to the first record in the the first group, and each time you READNEXT it merely reads the key pointed at and moves it along to the next key. Effectively, if you use EXECUTE 'SELECT...' you end up traversing the file twice: once to select the keys, and again to process them. Using the UniVerse Basic SELECT with READNEXT means that you only traverse it once, during processing. It also explains, however, why you cannot sort or apply selection criteria to your list.