Файлы
Данная лекция посвящена работе с файлами. Обычно файлом называют именованную (то есть имеющую имя) совокупность данных, записанных на диске. Файл состоит из компонентов (элементов). При чтении или записи файловая переменная перемещается к очередному компоненту и делает его доступным для обработки. Попробуем разобраться с тем, как можно работать с файлами в Прологе.
Для начала вспомним, что пользовательские файлы описываются в разделе описания доменов следующим образом:
file = <символическое имя файла1>;...; <символическое имя файлаN>
Обратите внимание, что при описании файловых доменов тип домена file располагается слева от равенства, а символические имена файлов — справа. Их еще называют внутренними или логическими именами файлов, в отличие от внешних или физических имен файлов. Символическое имя файла должно начинаться со строчной буквы.
Кроме пользовательских файлов, имеются стандартные файлы (или устройства), которые не нужно описывать в разделе описания доменов. Это:
-
stdin(стандартное устройство ввода);
-
stdout(стандартное устройство вывода);
-
stderror(стандартное устройство вывода сообщений об ошибках);
-
keyboard(клавиатура);
-
screen(монитор);
-
printer(параллельный порт принтера);
-
coml(последовательный порт).
По умолчанию стандартным устройством ввода является клавиатура, а стандартным устройством вывода — монитор. Чтобы начать работу с пользовательским файлом, его нужно открыть, а по завершении работы— закрыть. Стандартные устройства ввода/вывода, а также параллельный и последовательный порты открывать и закрывать не нужно.
Далее мы познакомимся со встроенными предикатами Турбо Пролога, с помощью которых можно осуществлять операции открытия и закрытия файлов, а также многие другие операции с файлами.
Начнем наше знакомство со встроенных предикатов, предназначенных для открытия файлов. Каждый из следующих четырех предикатов имеет два входных параметра. Первый параметр — это внутреннее символическое имя, указанное в разделе описания доменов, второй параметр — это строка, представляющая внешнее имя файла.
Предикат openread открывает файл только для чтения. Если файл с указанным внешним именем не будет обнаружен, предикат терпит неудачу и выводит соответствующее сообщение об ошибке.
Предикат openwrite открывает файл только для записи. Этот предикат создает на диске новый файл. Если файл с указанным внешним именем уже существует, он будет стерт. Если по какой-то причине файл не может быть создан, предикат терпит неудачу и выводит соответствующее сообщение об ошибке.
Предикат openappend открывает файл только для дозаписи в конец файла. Если файл с указанным именем не будет обнаружен, предикат выводит соответствующее сообщение об ошибке.
Предикат openmodify открывает файл для чтения и записи одновременно. Если файл с указанным именем не будет обнаружен, предикат выводит соответствующее сообщение об ошибке.
Для того чтобы проверить, существует ли файл с указанным именем в указанном месте, используется предикат existfile. Этот предикат имеет один аргумент. Предикат истинен, если файл с именем, указанным в качестве его единственного параметра, существует, и ложен — в противном случае.
Обратите внимание на то, что эти предикаты, связывают символическое имя файла с физическим именем открываемого файла. Поэтому, в отличие от других языков программирования, например Паскаля, нам нет необходимости перед операцией открытия файла проводить операцию связывания внутреннего и внешнего имен файла.
Поскольку символ "\", обычно используемый для разделения имен каталогов, применяется в Турбо Прологе для записи кодов символов, требуется использовать вместо одного обратного слэша два ("\\"). Например, чтобы указать путь "C:\Prolog\BIN", нужно записать строку "C:\\Prolog\\BIN".
Пример. Напишем замену для стандартного предиката openread. Предикат, который будет открывать файл на чтение (в случае, если он существует) и выводить сообщение о том, что файл с таким именем не найден (иначе). Этот предикат, как и предикат openread, будет иметь два аргумента.
Первым аргументом будет внутреннее символическое имя файла, вторым — строка, представляющая внешнее дисковое имя файла. Наша модификация предиката должна быть корректной и завершаться успехом в любом случае, вне зависимости от того, наличествует открываемый файл или отсутствует.
При реализации этого предиката воспользуемся встроенными предикатами: existfile для проверки существования; предикатом openread для открытия существующего файла на чтение; предикатом write для вывода сообщения.
openFile(F,N):– existfile(N),!, /* проверяем существование файла с именем N */ openread(F,N). /* связываем внешний файл с именем N с файловой переменной F и открываем его на чтение */ openFile(_,N):– write("Файл с именем ",N," не найден!"). /* выдаем сообщение, если предикат existfile потерпел неудачу */
Аналогичным образом можно модифицировать предикаты openappend и openmodify. Предикат openwrite можно модифицировать таким образом, чтобы при попытке открыть существующий файл на запись предикат вначале выдавал бы предупреждение о том, что содержимое этого файла будет уничтожено.
Для того чтобы корректно закрыть открытый файл, используется предикат closefile. В качестве его единственного параметра указывается символическое имя файла. Предикат в любом случае успешен, даже если соответствующий файл не был открыт.
С закрытым файлом можно работать только целиком. Он может быть переименован или удален с помощью предикатов renamefile и deletefile.
Предикат deletefile удаляет файл, указанный в качестве его единственного параметра. Если по какой-то причине удалить файл не получается, этот предикат выдает сообщение об ошибке.
Предикат renamefile изменяет имя файла, указанного в качестве его первого параметра, на имя, указанное в качестве его второго параметра. Если не существует файла, чье имя указано в первом параметре, или существует файл, чье имя указано во втором параметре, предикат выдаст сообщение об ошибке.
Предикат disk позволяет задать или узнать текущий диск и/или каталог, в зависимости от того, связан его единственный аргумент или свободен.
Кроме того, имеется предикат dir, который позволяет выбрать из списка файлы, соответствующие шаблону, указанному в качестве второго параметра этого предиката, находящиеся в каталоге, указанном в первом параметре. Выбранное нажатием клавиши Enter имя попадает в переменную, чье имя указано в качестве третьего параметра этого предиката.
Есть еще вариант этого предиката, имеющий три дополнительных входных параметра: четвертый — включает/отключает отображение подкаталогов; пятый — разрешает/запрещает изменять нажатием клавиши F4 шаблон, в соответствии с которым отображаются файлы; шестой — разрешает/запрещает отображение пути в заголовке окна. Ноль в четвертом, пятом и шестом параметрах означает запрет соответствующей опции, ненулевое значение — разрешение.
Предикат eof (сокращение от End Of File — "конец файла") успешен, если достигнут конец файла, в противном случае он неуспешен. В качестве его единственного входного параметра указывается символическое имя файла. Он обычно используется при организации рекурсивного считывания всех компонентов файла. Если его попытаться применить к файлу, открытому на запись, будет выдано сообщение об ошибке.
Содержимое файла можно рассматривать как поток компонентов. Каждый компонент файла находится на какой-то позиции. Для того чтобы узнать текущую позицию чтения или записи в файле, либо для того, чтобы изменить эту позицию, служит предикат filepos.
У него три аргумента. Первый аргумент — это символическое имя файла, второй — позиция внутри первого аргумента, которую нужно узнать или установить, третий — номер режима, который задает, откуда отсчитывается позиция.
Номер режима может принимать одно из трех значений: ноль, единица или двойка.
Если он равен нулю, то позиция считается от начала файла. Если он равен единице, позиция отсчитывается от текущей позиции. Этот режим имеет смысл только при означенном втором аргументе, когда предикат используется для изменения текущей позиции. Потому что если второй аргумент предиката не был означен, а третий аргумент равен единице, то во второй аргумент будет помещен ноль.
Смещение текущей позиции чтения или записи в файле относительно текущей позиции, естественно, равно нулю. Если же третий аргумент равен двойке, то позиция отсчитывается от конца файла.
Предикат может быть использован двояко. Если все три его аргумента связаны, то позиция, из которой осуществляется чтение или в которую производится запись, будет изменена в соответствии с числом, которым означен второй аргумент. Если его второй аргумент свободен, а первый и третий связаны, то второй аргумент будет означен текущей позицией чтения или записи.
Опишем ниже два предиката, служащие для перенаправления потоков ввода-вывода.
Для переопределения текущего устройства ввода или для того, чтобы узнать, какое устройства ввода является текущим, служит предикат readdevice. Этот предикат имеет один параметр, в качестве которого указывается символическое имя файла. Если предикат readdevice используется с несвязанным параметром, то он возвращает в него имя устройства, которое в настоящий момент является активным устройством чтения информации. Если его параметр означен именем открытого для чтения устройства, то этот предикат переопределит активное устройство ввода информации.
Предикат writedevice подобен предикату readdevice, однако используется для переопределения текущего устройства вывода или для получения имени текущего устройства вывода. Он также имеет один параметр, который может быть использован либо как входной, либо как выходной. В первом случае будет переопределено текущее устройство записи информации (должно быть открыто на запись или дозапись), во втором — параметр будет связан с именем активного устройства вывода.
Для записи данных в текущее устройство записи служит уже знакомый нам предикат write, который до этого мы использовали для вывода информации на монитор. Для чтения информации из активного устройства вывода — также уже знакомые нам предикаты readln (читает строку), readint (читает целое), readreal (читает вещественное число), readchar (читает символ), readterm (читает терм).
Имеется также предикат file_str, который целиком читает символы файла в строку или, наоборот, записывает содержимое строки в файл, в зависимости от того, свободен ли второй параметр этого предиката. Первым входным параметром этого предиката является символическое имя файла, а вторым — строка, в которую считывается содержимое файла или из которой записывается информация в него.
Предикат flush используется для принудительной записи в файл содержимого внутреннего буфера, выделенного для файла, указанного в его единственном параметре. Обычно он используется при работе с принтером.
И, наконец, последний встроенный предикат, о котором будет упомянуто в этой лекции, это предикат filemode. Он позволяет узнать или задать режим доступа к файлу. У этого предиката два параметра. Первый параметр — символическое имя файла, второй параметр задает или принимает режим работы с файлом, имя которого указано в первом параметре. Если второй параметр свободен, то он будет означен текущим режимом работы с файлом, а если связан, то указанный режим будет установлен. Второй параметр может принимать одно из двух значений. Значение ноль соответствует двоичному (бинарному) режиму работы с файлом, а значение единица — текстовому режиму. В текстовом режиме к строкам добавляются символы возврата каретки и перевода строки. Чаще используется текстовый режим, поскольку при работе в двоичном режиме данные можно записывать только посимвольно.
Рассмотрим на примерах, как можно использовать описанные предикаты.
Пример. Начнем с предиката, выводящего содержимое произвольного файла на экран. Предикат будет иметь один параметр строкового типа, представляющий собой внешнее дисковое имя файла.
При реализации этого предиката воспользуемся встроенными предикатами: existfile для проверки наличия файла на диске; openread для открытия существующего файла на чтение; readdevice для перенаправления ввода; eof для проверки, не исчерпали ли мы содержимое файла; readchar для чтения символов из файла, write для вывода прочитанного символа на экран.
Сначала напишем вспомогательный предикат, который будет читать символы из файла и выводить их на экран до тех пор, пока не будет достигнут конец файла.
Основной предикат будет проверять существование файла, указанного в качестве его параметра. Если файла с таким именем не существует, будет выдано соответствующее сообщение. Если файл с указанным именем существует, откроем его на чтение предикатом openread и определим его в качестве текущего устройства ввода информации, используя предикат readdevice. Далее воспользуемся нашим вспомогательным предикатом для вывода содержимого файла на экран. Закроем файл предикатом closefile. Сделаем текущим устройством чтения клавиатуру, воспользовавшись предикатом readdevice. В принципе, можно на этом остановиться, а можно организовать паузу до нажатия любой клавиши предикатом readchar(_). До этого неплохо бы вывести сообщение о том, что работа будет продолжена после нажатия любой клавиши.
Так как это — наш первый опыт работы с файлами в Прологе, приведем всю программу целиком, со всеми разделами. В разделе описания внутренней цели организуем ввод имени файла и вызов основного предиката.
DOMAINS /* раздел описания доменов */ file = f /* f — внутреннее имя файла */ PREDICATES /* раздел описания предикатов */ write_file(file) writeFile(string) CLAUSES /* раздел описания предложений */ write_file(f):– not(eof(f)),!, /* если файл f еще не закончился */ readchar(C), /* читаем из него символ */ write(C," "), /* выводим символ на экран*/ write_file(f). /* продолжаем процесс */ write_file(_). /* это предложение нужно, чтобы предикат не потерпел неудачу в случае, когда будет достигнут конец файла */ writeFile(F_N):– existfile(F_N),!, /* убеждаемся в существовании файла с именем F_N */ openread(f,F_N), /* связываем внешний файл F_N с внутренним файлом f и открываем его на чтение */ readdevice(f), /* устанавливаем в качестве устройства чтения файл f */ write_file(f), /* вызываем предикат, выводящий на экран все символы файла f */ closefile(f), /* закрываем файл */ readdevice(keyboard), /* перенаправляем ввод на клавиатуру */ nl,nl, /* пропускаем строку */ write("Нажмите любую клавишу"), /* выводим сообщение на экран */ readchar(_)./* ждем нажатия любой клавиши */ writeFile(F_N):– write("Файл с именем ",F_N," не наден!"). /* выдаем сообщение, если предикат existfile потерпел неудачу */ GOAL /* раздел описания внутренней цели*/ write("Введите имя файла: "), readln(F_N), /* читаем название файла в переменную F_N */ writeFile(F_N).
Пример. Теперь создадим предикат, который будет формировать файл из символов, вводимых с клавиатуры. Предикат будет иметь один параметр, представляющий собой внутреннее имя файла.
Начнем реализацию предиката с того, что сделаем экран текущим устройством вывода информации, выведем пользователю предложение ввести символ, сообщим о том, какой символ будет завершать ввод. Прочитаем символ с клавиатуры, выведем его на экран и сравним с тем символом, который завершает ввод. Если символы совпадают, закроем файл. Иначе: сделаем текущим устройством записи файл, запишем символ в файл, вызовем основной предикат.
Еще раз процитируем программу целиком, а в дальнейшем будем приводить только основной предикат. В разделе описания внутренней цели организуем ввод имени файла, открытие файла на запись и вызов основного предиката.
DOMAINS file=f PREDICATES Readfile CLAUSES readfile:– writedevice(screen), /* назначаем текущим устройством записи экран */ write("Введите символ (# — конец ввода)"), nl, /* выводим сообщение */ readchar(C), /* читаем символ с клавиатуры */ write(C), /* дублируем символ на экран */ C<>'#',!, /* если это не #*/ writedevice(f), /* , то перенаправляем вывод в файл */ write(C), /* записываем символ в файл */ readfile. readfile:– closefile(f). /* если введенный символ оказался равен символу '#', то закрываем файл */ GOAL write("Введите имя файла: "), readln(F_N), /* читаем название файла в переменную F_N */ openwrite(f,F_N), /* связываем внешний файл F_N с внутренним файлом f и открываем его на запись */ readfile(f).
Попробуйте изменить эту программу так, чтобы в файл записывались с клавиатуры не символы, а целые строки. При этом не забудьте при записи строки в файл добавить символы конца строки и перевода каретки. Это можно сделать с помощью встроенного предиката nl или сцепив записываемую строку с двумя символами с кодами 13 и 10.
Пример. Запишем предикат, который будет выводить содержимое файла на экран и принтер. Предикат будет иметь один параметр, представляющий собой внутреннее имя файла.
Напишем только этот предикат, оставив "за кадром" проверку на существование внешнего файла, его открытие на чтение. Все это мы делали не раз. Наш предикат должен проверять, не достигнут ли конец файла. В случае достижения конца файла его нужно закрыть. Если мы еще не добрались до конца файла, читаем символ, используя предикат readchar. Выводим его на экран посредством предиката write, перенаправляем вывод на принтер предикатом writedevice, выводим символ на принтер, заставляем принтер немедленно напечатать символ, используя предикат flush, устанавливаем для последующего вывода символа текущим устройством записи информации экран. Повторяем этот процесс, пока не будут исчерпаны все символы исходного файла.
writeFile_to_scr_and_pr(f):– not(eof(f)),!, /* если файл f еще не закончился */ readchar(C), /* читаем из файла f символ в переменную C */ write(C), /* выводим символ C на экран */ writedevice(printer), /* устанавливаем текущим устройством записи принтер*/ write(C), /* выводим символ C на экран */ flush(printer), /* сбрасываем данные из буфера на принтер */ writedevice(screen), /* перенаправляем вывод на экран*/ writeFile_to_scr_and_pr(f). /* продолжаем */ writeFile_to_scr_and_pr:– closefile(f). /* закрываем файл f */
Обратите внимание на использование стандартного предиката flush. Как правило, принтер выводит данные на печать только тогда, когда будет заполнен его внутренний буфер. Предикат flush позволяет осуществить немедленный вывод на печатающее устройство накопленной в буфере информации.
Попробуйте изменить этот предикат так, чтобы чтение из файла и, соответственно, вывод на экран и принтер осуществлялись не посимвольно, а построчно. Для этого замените предикат readchar на предикат readln, а также не забудьте добавить символы конца строки и перевода каретки при выводе.
Пример. Напишем предикат, который будет переписывать компоненты одного файла в другой файл так, чтобы в итоговом файле все английские буквы были большими. У предиката будет два аргумента строкового типа.
Первым параметром будет внешнее имя исходного файла, вторым параметром — внешнее имя итогового файла.
Начнем с создания вспомогательного предиката, который будет читать из первого файла строки, переводить их символы в верхний регистр, используя встроенный предикат upper_lower, а полученные строки записывать во второй файл. Когда первый файл буден исчерпан, этот предикат должен закрыть оба файла.
Основной предикат будет проверять существование исходного файла. Если файла с указанным именем не существует, он должен вывести соответствующее сообщение. Если файл существует, он должен быть открыт на чтение. Итоговый файл должен быть открыт на запись, первый файл установлен в качестве текущего устройства чтения информации, второй — в качестве текущего устройства записи информации. После этого остается только вызвать вспомогательный предикат.
Запишем эти два предиката. Надеюсь, что недостающие разделы описаний читатели уже в состоянии дописать самостоятельно.
transform:– not(eof(f)),!, /* если файл f еще не закончился */ readln(S), /* читаем из файла f строку в переменную S */ upper_lower(S_U,S), /* S_U — результат преобразования всех символов строки S в верхний егистр */ write(S_U),nl, /* записываем строку S_U в файл f_o */ transform. /* продолжаем процесс */ transform:– closefile(f), /* закрываем файл f */ closefile(f_o). /* закрываем файл f_o */ upper_file(N_F,N_o_F):– existfile(N_F),!, /* проверяем существование файла с именем N_F */ openread(f,N_F), /* связываем внешний файл с именем N_F с внутренним файлом f и открываем его на чтение */ readdevice(f), /* устанавливаем в качестве текущего устройства чтения файл f */ openwrite(f_o,N_o_F), /* связываем внешний файл с именем N_o_F с внутренним файлом f и открываем его на запись */ writedevice(f_o), /* устанавливаем в качестве текущего устройства записи файл f_o */ transform. /* вызываем вспомогательный предикат */ upper_file(N_F,_):– write("Файл с именем ",N_F," не наден!"). /* выдаем сообщение, если предикат existfile был неуспешен */