从文件中读取内容
本节将学习如何从文件中读取内容。如果文件中的内容是以Prolog的语句形式存在的,那么在Prolog中读取这样的文件内容是很容易的。比如文件houses.txt的内容如下:
gryffindor.
hufflepuff.
ravenclaw.
slytherin.
下面是Prolog打开文件,读取内容,并且将内容显示在屏幕上的代码:
main :-
open('houses.txt', read, Str),
read(Str, House1),
read(Str, House2),
read(Str, House3),
read(Str, House4),
close(Str),
write([House1, House2, House3, House4]), nl.
上面代码将会以只读模式打开文件,然后使用Prolog内置谓词read/2读取Prolog语句,然后关闭流,最后打印信息到屏幕上去。
这种方式是很直接和简单的。但是,read/2谓词需要谨慎地使用。首先,它只能处理Prolog的语句(我们将会在后面讨论更多这方面的内容),第二,如果流没有任何内容了,它可能会导致运行时错误。有没有一种更加优雅的方式可以克服第二个问题呢?
当然有的。内置谓词at_end_of_stream/1能够检查stream是否已经到达了尾端,而且是以一种安全的方式进行使用。对于一个流X,at_end_of_stream(X)当流X已经到达了其尾端时为真(换种说法,文件中所有的语句都已经被读取了)。
下面的代码是经过修改后的版本,展示了如何使用at_end_of_stream/1这个谓词:
main :-
open('houses.txt', read, Str),
read_houses(Str, Houses),
close(Str),
write(Houses), nl.
read_houses(Stream, []) :-
at_end_of_stream(Stream).
read_houses(Stream, [X|L]) :-
+ at_end_of_stream(Stream),
read(Stream, X),
read_houses(Stream, L).
现在来解决更加麻烦的问题。上面提及read/2只能读取Prolog语句。如果你想要读取任意文件内容,情况可能会变得比较复杂,因为Prolog会迫使读入的内容以字符级别来进行,内置的谓词get_code/2从流中读取下一个存在的字符。字符在Prolog中是使用其整数数字来代替的。比如,get_code/2将会在流中读取字符a,然后返回结果是97。
通常,我们不会关心这些整数,而是关心字符本身——或者,由这些字符组成的列表,来表示的原子。我们如何处理这些字符呢?一种方式是使用内置谓词atom_codes/2,可以将整数列表转换为对应的原子。我们将使用下一个例子介绍这种方式,例子展示了如何从流中读取单词:
readWord(InStream, W) :-
get_code(InStream, Char),
checkCharAndReadRest(Char, Chars, InStream),
atom_codes(W, Chars).
checkCharAndReadRest(10, [], _) :- !.
checkCharAndReadRest(32, [], _) :- !.
checkCharAndReadRest(-1, [], _) :- !.
checkCharAndReadRest(end_of_file, [], _) :- !.
checkCharAndReadRest(Char, [Char|Chars], InStream) :-
get_code(InStream, NextChar),
checkCharAndReadRest(NextChar, Chars, InStream).
代码是如何工作的?它读取一个字符然后检查这个字符是否是空白(整数为32),是否为分行符(整数为10),或者是流的结尾(整数为-1),以上任意一种情况下都会被当成一个完整单词的结束,否则的话下一个字符将会进行读取。
将内容写入文件
许多的应用程序都需要将输出写入到文件中进行保存,而不仅仅是显示在屏幕上。本节我们将学习如何在Prolog中将输出内容写入到文件中。
为了写文件,我们必须创建一个(或者打开一个)文件并且将一个流与其相关联。你可以认为流就是文件的连接。在Prolog中,流的表现形式很不友好,名字都是类似“$stream(1833680)”这样可读性很差的。幸运的是,你不会直接使用流的名字,虽然Prolog在内部为其分配了名字,你可以通过使用Prolog的合一去匹配流名字和一个变量,然后使用变量操作流,而不是Prolog中内部分配给流的名字。
如果你想要输出字符串“Hogwarts”到文件hogwarts.txt中,可以这么做:
...
open('hogwarts.txt', write, Stream),
write(Stream, 'Hogwarts'), nl(Stream),
close(Stream),
...
如何理解上面的代码?首先,内置的谓词open/3将会创建新的文件:hogwarts.txt;open/3的第二个参数指出我们想要打开一个新的文件(或者覆盖任何已经存在的,相同名字的文件);open/3的第三个参数返回流的名字。其次,我们将“Hogwarts”写入到流中,并且加入新的一行。最后,我们使用内置谓词close/1关闭掉流。
这就是写文件的操作。正如之前承诺的,我们对流的名字不感兴趣——我们使用合一的变量操作流。还有需要注意的是,谓词write/2是一个更加基础的版本,因为在第九章中使用了write/1将内容输出到屏幕。
如果你不希望复写已经存在的文件,而是在已经存在的文件中添加新的内容呢?可以选择打开文件的方式(不再是write方式)来做到,使用append作为open/3的第二个参数。如果给定名字的文件还不存在,这种模式将会创建一个新的文件。