Чтение и запись файлов

До сих пор мы работали с вводом-выводом, печатая на терминале и считывая с него. Ну а как читать и записывать файлы? В некотором смысле мы уже работали с файлами. Чтение с терминала можно представить как чтение из специального файла. То же верно и для печати на терминале – это почти что запись в файл. Два файла – stdin и stdout – обозначают, соответственно, стандартный ввод и вывод. Принимая это во внимание, мы увидим, что запись и чтение из файлов очень похожи на запись в стандартный вывод и чтение со стандартного входа.

Для начала напишем очень простую программу, которая открывает файл с именем girlfriend.txt и печатает его на терминале. В этом файле записаны слова лучшего хита Авриль Лавин, «Girlfriend». Вот содержимое girlfriend.txt:

Эй! Ты! Эй! Ты!

Мне не нравится твоя подружка!

Однозначно! Однозначно!

Думаю, тебе нужна другая!

Программа:

import System.IO

main = do

   handle <– openFile "girlfriend.txt" ReadMode

   contents <– hGetContents handle

   putStr contents

    hClose handle

Скомпилировав и запустив её, получаем ожидаемый результат:

Эй! Ты! Эй! Ты!

Мне не нравится твоя подружка!

Однозначно! Однозначно!

Думаю, тебе нужна другая!

Посмотрим, что у нас тут? Первая строка – это просто четыре восклицания: они привлекают наше внимание. Во второй строке Авриль сообщает вам, что ей не нравится ваша подружка. Третья строка подчёркивает, что неприятие это категорическое. Ну а четвёртая предписывает подружиться с кем-нибудь получше.

А теперь пройдёмся по каждой строке кода. Наша программа – это несколько действий ввода-вывода, «склеенных» с помощью блока do. В первой строке блока do мы использовали новую функцию, openFile. Вот её сигнатура: openFile :: FilePath –> IOMode –> IO Handle. Если попробовать это прочитать, получится следующее: «Функция openFile принимает путь к файлу и режим открытия файла (IOMode) и возвращает действие ввода-вывода, которое откроет файл, получит дескриптор файла и заключит его в результат».

Тип FilePath – это просто синоним для типа String; он определён так:

type FilePath = String

Тип IOMode определён так:

data IOMode = ReadMode | WriteMode | AppendMode | ReadWriteMode

Этот тип содержит перечисление режимов открытия файла, так же как наш тип содержал перечисление дней недели. Очень просто! Обратите внимание, что этот тип – IOMode; не путайте его с IO Mode. Тип IO Mode может быть типом действия ввода-вывода, которое возвращает результат типа Mode, но тип IOMode – это просто перечисление.

В конце концов функция вернёт действие ввода-вывода, которое откроет указанный файл в указанном режиме. Если мы привяжем это действие к имени, то получим дескриптор файла (Handle). Значение типа Handle описывает, где находится наш файл. Мы будем использовать дескриптор для того, чтобы знать, из какого файла читать. Было бы глупо открыть файл и не связать дескриптор файла с именем, потому что с ним потом ничего нельзя будет сделать! В нашем случае мы связали дескриптор с идентификатором handle.

На следующей строке мы видим функцию hGetContents. Она принимает значение типа Handle; таким образом, она знает, с каким файлом работать, и возвращает значение типа IO String – действие ввода-вывода, которое вернёт содержимое файла в результате. Функция похожа на функцию getContents. Единственное отличие – функция getContents читает со стандартного входа (то есть с терминала), в то время как функция hGetContents принимает дескриптор файла, из которого будет происходить чтение. Во всех остальных смыслах они работают одинаково. Так же как и getContents, наша функция hGetContents не пытается прочитать весь файл целиком и сохранить его в памяти, но читает его по мере необходимости. Это очень удобно, поскольку мы можем считать, что идентификатор contents хранит всё содержимое файла, но на самом деле содержимого файла в памяти нет. Так что даже чтение из очень больших файлов не отожрёт всю память, но будет считывать только то, что нужно, и тогда, когда нужно.

Обратите внимание на разницу между дескриптором, который используется для идентификации файла, и его содержимым. В нашей программе они привязываются к именам handle и contents. Дескриптор – это нечто, с помощью чего мы знаем, что есть наш файл. Если представить всю файловую систему в виде очень большой книги, а каждый файл в виде главы, то дескриптор будет чем-то вроде закладки, которая показывает нам, где мы в данный момент читаем (или пишем), в то время как идентификатор contents будет содержать саму главу.

С помощью вызова putStr contents мы распечатываем содержимое на стандартном выводе, а затем выполняем функцию hClose, которая принимает дескриптор и возвращает действие ввода-вывода, закрывающее файл. После открытия файла с помощью функции openFile вы должны закрывать файлы самостоятельно!