Warp factor 8, Mr. Data. (DATA statement in BASIC) (Programming Power) (Column)
by Tom Campbell
While you take it for granted now, BASIC didn't always have the ability to read files. Hard to imagine BASIC without this crucial aspect of the language, but the first versions of Microsoft BASIC had to be squeezed into 4K. That's right--less than the minimum disk space required for any file on some high-capacity hard drives.
DATA statements were added as a sop. For reasons we'll examine shortly, I rarely employ them in BASIC programs anymore, but my first excursion into a higher-level language--and in 1984, Turbo Pascal was undeniably a higher-level language than GW-BASIC--left me amazed that there was no way to include data in a program. You can now use Turbo's strangely named typed constants to do roughly the same thing as DATA statements, but they were added years later as a response to C's initialized variables. I was depressed to realize that my only recourse was either to declare a bunch of variables at the top of a routine and initialize them manually at a later time or to use files.
Make my DATA
A DATA statement is just a list of one or more values of a type recognized by BASIC. The READ statement automatically assigns one of the values in the DATA statement to a variable, magically keeping track of both the line of the DATA statement and the datum being read. Then it moves efficiently on to the next datum the next time it's called, using an internal placeholder called the DATA pointer.
The program PLAY1.BAS offers an example of DATA at work, as well as DATA's assistant, RESTORE. Run it, and you'll hear a passable rendition of reveille, the familiar bugle call played by a lone, shivering enlisted man at dawn in countless war movies.
'PLAY1.BAS--tested with 'PowerBASIC and QuickBASIC. 'Illustrates DATA and RESTORE 'statements by playing reveille. 'THE FIRST PART. 'Each call to PlayVerse will read 'each NextLine$ string and play 'it until a "" occurs. CALL PlayVerse CALL PlayVerse 'THE SECOND PART. 'Go back to the first DATA 'statement. Play up to a "". RESTORE CALL PlayVerse 'Skip down to the Part 2 DATA 'statement, and play up to a "". RESTORE Part2 CALL PlayVerse 'THE THIRD PART. 'Simply continue playing Part 3. CALL PlayVerse 'This loop fetches the value of 'each DATA statement, copies it 'into the string variable 'NextLine$, and plays it. When 'the variable is a null string, 'the subroutine exits. SUB PlayVerse DO READ NextLine$ PLAY NextLine$ LOOP UNTIL NextLine$ = "" END SUB 'Refrain--this is used several 'times by reveille. DATA "P8 C32 P32 P16 F32 P16 L16 A F C32" DATA "P16 C32 P32 P16 F32 P16 L16 A F C16" DATA "P16 C32 P32 P16 F32 P16 L16 A F C16" DATA ""
'Part 1--this is used the first 'time through. DATA "P16 L16 F P16 L4 A F8 P8" DATA "" 'Part 2--used the second time 'through. Part2: DATA "P16 C16 P16 L4 F" DATA "" 'Part3--used as the third and 'last part of reveille. DATA "MS P8 L8 A A A A A L4 O5 C" DATA "L8 O4 A F A F A F" DATA "L8 A A A A L4 O5 C" DATA "L8 O4 A F A F C C L4 F" DATA ""
At the heart of PLAY1.BAS is the PlayVerse subroutine. It fetches each string found in a DATA statement (BASIC knows to start the DATA pointer at the first DATA statement in your program), copies that string into the variable NextLine$, and then uses PLAY on that string variable. It stops when an empty string is encountered. The empty string as used here, by the way, is referred to in the literature as a sentinel value. A sentinel value is a user-defined value that cannot possibly occur in a valid list of data and can therefore be used to stop a sequence of actions (normally data entry, as in this example). You'll often see -1, 0, or a large number such as 9999 used for the same purpose in DATA statements that use numeric values.
The first two calls to PlayVerse together play the first of reveille's three parts. The first call plays the section labeled Refrain, a section used by all three parts of the bugle call. At this point, the DATA pointer points to the DATA comment as Part 1, and that's what gets played on the second call to PlayVerse, right up to its null (sentinel) string.
As we get ready to play Part 2 of reveille, the utility of the DATA and RESTORE statements becomes clear. The second part of reveille reprises the first section, which is labeled Refrain. We could take the easy way out and just copy the DATA statements, but BASIC was originally designed with 4K or 8K free system memory in mind, not 640K. The RESTORE statement allowed you to reset the DATA pointer back to the first DATA statement. We'll quickly see that this wouldn't really do the job for this program, but luckily RESTORE was later supplemented to allow you to restore to a particular line number, meaning that the DATA pointer would now point at the DATA statement on the given line. Later, an alphanumeric label (like Refrain in this example) could also be used.
So to play the second part, RESTORE moves the DATA pointer to the first DATA statement. However, this time we want to play the data statements labeled Part 2 after we play Refrain, not Part 1.
After the refrain is played, a RESTORE Part2 allows us to skip over Part 1 and go directly to the Part2 label when a subsequent call to PlayVerse executes its first READ statement.
File Your Data
So far, so good. We've seen how DATA statements let us embed initialized data into a program, which no popular language until C would allow, several years after BASIC's rise to unprecedented popularity. We've also seen how to reorganize the sequence of this data by using the RESTORE statement with a line label (or number, for the Philistines reading this column). Why bother with files?
Because files allow the crucial separation of program and data--that's why. As you can see, the sample program PLAY1.BAS, above, is written for a BASIC compiler. If you decide to change the tune it plays or to give it an option to play more than one tune, you'll be confronted by a morass of DATA statements that could haunt you for the entire life of the project. Every time you want to add or edit a song, you'll have to recompile. Worse, the people to whom you distribute your program would have no way to add or change tunes themselves, unless they had the source code to your program and a BASIC compiler and they knew how to program.
No, DATA statements are only helpful when the data set is small, does not change, and is only used once. For example, some BASIC programs contain short subprograms in machine language. If they total a page or less of source code, converting them into DATA statements isn't a bad idea. Otherwise, they should reside in separate object modules.
The solution to data that changes while the program doesn't is to use files. First the good news: They're very easy to use once you get the hang of file I/O statements and maintenance. Now the bad news: The data in sequential files (as in this example) is used in a continuous stream from top to bottom, and the only way to change position is to start back at the beginning of the file.
These compromises are well worth the limitations, and you can always use random files for more sophisticated manipulation of the file pointer (eliminating the ability to use ASCII files, though).
Play It Again
Here is the new program, with a data file following it. Save the data file as an ASCII file under the name PLAY.DAT.
Make sure this file ends with a blank line! Otherwise, the loop won't stop because the sentinel will never be found. When you want to add a song, just place it in a different file under a new name and pass that name to the PlayTune subroutine.
DECLARE SUB PlayTune (Filename$) 'PLAY2.BAS--tested with 'PowerBASIC and QuickBASIC. 'Illustrates separation of program 'and data by placing the 'tune to be played in as sequential 'ASCII file. Read in the file 'PLAY.DAT and play it. Stop 'when a blank line 'is encountered. CALL PlayTune ("PLAY.DAT") 'PlayTune opens the ASCII data 'file Filename$ and reads in the 'music data to be played, playing 'each string until a null string '(blank line) is hit. 'End your file with a blank line! 'Filename$ is an ASCII data file. 'Make it usabel for reading. 'Loop until the sentinel value '(a blank line) is encountered. 'Read in a line. Play it unless 'it's blank. In that case, quit. 'Return the file resources to DOS. SUB PlayTune(Filename$) OPEN Filename$ FOR INPUT AS #1 DO LINE INPUT #1, NextLine$ PLAY NextLine$ LOOP UNTIL NextLine$ = "" CLOSE #1 END SUB
Here is the ASCII file PLAY.DAT. End it with a blank line and savie it as an ASCII file.
P8 C32 P32 P16 F32 P16 L16 A F C32 P16 C32 P32 P16 F32 P16 L16 A F C16 P16 C32 P32 P16 F32 P16 L16 A F C16 P16 L16 F P16 L4 A F8 P8 P8 C32 P32 P16 F32 P16 L16 A F C32 P16 C32 P32 P16 F32 P16 L16 A F C16 P16 C32 P32 P16 F32 P16 L16 A F C16 P16 C16 P16 L4 F MS P8 L8 A A A A A L4 O5 C L8 O4 A F A F A F L8 A A A A L4 O5 C L8 O4 A F A F C C L4 MS F