Previous | Table of Contents | Next |
6.3.5.3. Reading from Many Files
Multiple-file I/O is also given standard support. The easiest way of switching from one input file to another is with the predicates see and seen. see(Filename) takes a constant as a parameter and opens the file by the same name for reading. Any subsequent reads and get0s come from that file. The argument to see must be a constant, so for instance, if there are any dots in the file name, the entire name must be single-quoted, as in data.txt (not double-quoted because data.txt is a Prolog string). seen, which takes no arguments, closes the current input file and resets the input to come from the default source (generally, the terminal). A third predicate, seeing(X), instantiates its argument to the name of the file currently being seen.
The see/seeing/seen predicates have some state built into them. If you have previously been reading file A with see, then switch to file B with another see, and then later see file A again, you resume reading file A where you left off. Only when you have seen a file does it get closed completely and reopened again from the start with another see.
This behavior is particularly useful for including files in other files. Consider the following code:
process_file(Filename) :- seeing(Curr), see(Filename), process_lines, seen, see(Curr). process_lines :- readstring(Line, not_eof), !, handle_include(Line). process_lines. handle_include(Line) :- append(#include , Filenamestring, Line), !, name(Filename, Filenamestring), process_file(Filename), process_lines. handle_include(Line) :- process_line(Line), process_lines.
This code processes lines in a file, using the previous readstring code and following include links to other files. On every line in the original or included files, the predicate process_line is called; this can be anything we want. Note the use of append in one of its other useful instantiation patternswith the first and third arguments instantiated and the second uninstantiated.
The corresponding output routines to see/seeing/seen are tell/telling/told. They take the same number of arguments as the seen series and have similar behavior with regard to resuming previous I/O operations.
6.3.5.4. Stream I/O
It is also possible to open a file for reading or writing and obtain a handle with which to use it, called a stream. The predicate open(Filename, Mode, Stream) takes a constant file name and a mode (read, write, or append) and instantiates its third argument to a stream associated with that file.
The basic I/O routines read, write, nl, get0, and put all have variants that take a stream as an additional, first parameter. Thus read(Str1, Term) reads a term from stream Str1, and nl(Str2) writes a newline character on stream Str2. The one-parameter read is referred to as read/1, and the two-parameter version as read/2, consistent with the usual conventions for naming multi-arity predicates (see section 6.3.3). The predicate close(Stream) closes a previously opened input or output stream.
Here we focus on four logic/control constructs that come in handy from time to time: fail, true, ;, and the if-then-else construct.
fail simply fails, and true simply succeeds without having any other effect. fail is sometimes useful after a cut to say that the predicate should fail without trying other clauses. true is mostly useful in conjunction with the if-then-else, to be described later.
The ; operator means simply or. An example of its use is the following:
process_line(X) :- (X = exit ; X = quit ; X = done), report_termination.
This clause says that if X is either exit, quit, or done, the clause succeeds and reports termination (whatever that may mean in this context). Without the or operator, we would have had to write this as three separate clauses:
process_line(exit) :- report_termination. process_line(quit) :- report_termination. process_line(done) :- report_termination.
The or thus allows us to usefully condense code. Note that the precedence of the semicolon is higher than that of the comma so that if we had omitted the parentheses, the clause body would have been parsed as X = exit ; X = quit ; (X = done, report_termination).
Another useful condensing construct is the if-then-else construct. The expression (a -> b ; c), for instance, is a valid if-then-else goal. Its meaning is that if a succeeds, b must succeed, and if a fails, c must succeed. As you can see, it has the effect of performing a kind of cut within a single goal and can be used to avoid having to program the relatively meaningless auxiliary predicates that sometimes arise in Prolog code. For instance, the improved readstring predicate we wrote in section 6.3.5.2 can be simplified to
readstring(X, EOFflag) :- get0(C), ( C = -1 -> X = [], EOFflag = eof ; newlinechar(C) -> X = [], EOFflag = not_eof ; X = [C|Cs], readstring(Cs, EOFflag) ).
This packages up all the logic of readstring into one predicate, without requiring the auxiliary readstring1 predicate. Note that we have actually used if-then-else twice, with one occurrence nested inside the other. The comma has lowest precedence, so we should read the clause body as if there were parentheses around each comma-separated sequence of atoms.
Prolog is useful for finding a solution to a logically stated problem. Occasionally, in doing so, we have to find all solutions to some subproblem. There are three main predicates that we can use to do this: findall/3, bagof/3, and setof/3.
Previous | Table of Contents | Next |