— запись строки в файл
writeFile
:: FilePath -> String -> IO ()
— добавление строки в конеци файла
appendFile
:: FilePath -> String -> IO ()
Напишем программу, которая сначала запрашивает путь к файлу. Затем показывает его содержание. За-
тем запрашивает ввод строки из терминала. А после этого добавляет текст в конец файла.
main = msg1 >> getLine >>= read >>= append
where read
file = readFile file >>= putStrLn >> return file
append file = msg2 >> getLine >>= appendFile file
msg1
= putStr ”input file: ”
msg2
= putStr ”input text: ”
В самом левом вызове getLine мы читаем имя файла, затем оно используется в локальной функции
read. Там мы читаем содержание файла (readLine), выводим его на экран (putStrLn), и в самом конце мы
возвращаем из функции имя файла. Оно нам понадобится в следующей части программы, в которой мы
будем читать новые записи и добавлять их в файл. Новая запись читается функцией getLine в локальной
функции append.
Сохраним в модуле File. hs и посмотрим, что у нас получилось. Перед этим создадим в текущей дирек-
тории тестовый пустой файл под именем test. В него мы будем добавлять новые записи.
*Prelude> :l File
[1 of 1] Compiling File
( File. hs, interpreted )
Ok, modules loaded: File.
*File> main
input file: test
input text: Hello!
*File> main
input file: test
Hello!
input text: Hi)
*File> main
input file: test
Hello!Hi)
В самом начале наш файл пуст, поэтому сначала мы видим пустую строчку вместо содержания, но потом
мы начинаем добавлять в него новые записи.
Ленивое и энергичное чтение файлов
С чтением файлов связана одна тонкость. Функция readFile читает содержимое файла в ленивом стиле.
Подробнее о ленивой стратегии вычислений мы поговорим в следующей главе. По ка отметим, что readFile
не читает следующую порцию файла до тех пор пока она не понадобится в программе. Иногда это очень удоб-
но. Например мы можем читать содержание очень большого файла и составлять какую-нибудь статистику
на основе прочитанного текста. При этом в памяти будет храниться лишь малая часть файла. Но иногда
это свойство мешает. Рассмотрим такую задачу: перевернуть текст в файле под именем ”test”. Мы должны
сначала считать текст из файла, затем перевернуть его и в конце записать в тот же файл. Мы могли бы
написать эту программу так:
module Main where
main :: IO ()
main = inFile reverse ”test”
inFile :: (String -> String) -> FilePath -> IO ()
inFile fun file = writeFile file . fun =<< readFile file
Типичные задачи IO | 131
Функция inFile обновляет текст файла с помощью некоторого преобразование. Но если мы запустим эту
программу:
*Main> main
*** Exception: test: openFile: resource busy (file is locked)
Мы получили ошибку. Мы пытаемся писать в файл, который уже занят для чтения. Дело в том, что функ-
ция readFile заняла файл, за счёт чтения по кусочкам. Для решения этой проблемы необходимо воспользо-
ваться энергичной версией функции readFile, она будет читать файл целиком. Эта функция живёт в модуле
System.IO.Strict:
import qualified System.IO.Strict as StrictIO
inFile :: (String -> String) -> FilePath -> IO ()
inFile fun file = writeFile file . fun =<< StrictIO. readFile file
Функция main осталась прежней. Теперь наша программа спокойно переворачивает текст файла.
Аргументы программы
Пока программы, которые мы создавали просили пользователя ввести данные вручную при выполнении
программы, они работали в интерактивном режиме, но чаще всего программы принимают какие-нибудь
начальные данные, установки или флаги. Читать начальные данные можно с помощью функций из модуля
System.Environment.
Узнать, что передаётся в программу можно функцией getArgs :: IO [String]. Она возвращает список
строк. Это те строки, что мы написали за именем программы через пробел при вызове в терминале. Напишем
простую программу, которая распечатывает свои аргументы по порядку, в виде пронумерованного списка.
module Main where
import System.Environment
main = getArgs >>= mapM_ putStrLn . zipWith f [1 .. ]
where f n a = show n ++ ”: ” ++ a
В локальной функции f мы присоединяем к строке номер через двоеточие. Функцией mapM_ мы пробегаем
по списку строк, отображая их с помощью функции putStrLn. Обратите внимание на краткость программы,
с помощью функции композиции мы легко составили функцию, которая приписывает к аргументам числа, а
затем выводит их на экран.
Скомпилируем программу в интерпретаторе и вызовем её.
*Main> :! ghc —make Args
[1 of 1] Compiling Main
( Args. hs, Args. o )
Linking Args …
*Main> :! ./Args hey hey hey 23 54 ”qwe qwe qwe” fin
1: hey
2: hey
3: hey
4: 23
5: 54
6: qwe qwe qwe
7: fin
Если мы хотим, чтобы аргумент-строка содержал пробелы мы заключаем его в двойные кавычки.
С помощью функции getProgName можно узнать имя программы. Создадим программу, которая здоро-
вается при вызове. И отвечает в зависимости от настроения программы. Настроение задаётся аргументом
программы.
module Main where
import Control.Applicative
import System.Environment
main = putStrLn =<< reply getProgName getArgs
132 | Глава 8: IO
reply :: String -> [String] -> String
reply name (x:_) = hi name ++ case x of
”happy”
-> ”What a lovely day. What’s up?”
”sad”
-> ”Ooohh. Have you got some news for me?”
”neutral”
-> ”How are you?”
reply name _
= reply name [”neutral”]
hi :: String -> String
hi name = ”Hi! My name is ” ++ name ++ ”.n”
В функции reply мы составляем реплику программы. Она зависит от имени программы и поступающих
на вход аргументов. Посмотрим, что у нас получилось:
*Main> :! ghc —make HowAreYou.hs -o ninja
[1 of 1] Compiling Main
( HowAreYou. hs, HowAreYou. o )
Linking ninja …
*Main> :! ./ninja happy
Hi! My name is ninja.
What a lovely day. What’s up?
*Main> :! ./ninja sad
Hi! My name is ninja.
Ooohh. Have you got some news for me?
Вызов других программ
Мы можем вызвать любую программу из нашей программы. Это делается с помощью функции system,
которая живёт в модуле System.
system :: String -> IO ExitCode
Она принимает строку и запускает её в терминале. Так же как мы делали это с помощью приставки :! в
интерпретаторе. Значение типа ExitCode говорит о результате выполнения строки. Он может быть успешным,
тогда функция вернёт ExitSuccess и закончиться ошибкой, тогда мы сможем узнать код ошибки по значению
ExitFailure Int.
Случайные значения
Функции для создания случайных значений определены в модуле System.Random. Модуль System.Random
входит в библиотеку random. Если в вашей поставке ghc его не оказалось, вы можете установить его вручную
через интернет, набрав в командной строке cabal install random. Сначала давайте разберёмся как гене-
рируются случайные числа. Стандартные случайные числа очень похожи на те, что были у нас, когда мы
рассматривали примеры специальных функций. У нас есть генератор случайных чисел типа g и с помощью
функции next мы можем получить обновлённый генератор и случайное целое число:
next :: g -> (Int, g)
Не правда ли этот тип очень похож на тип результата функций с состоянием. В качестве состояния теперь
выступает генератор случайных чисел g. Это поведение описывается классом RandomGen:





