Шрифт:
Интервал:
Закладка:
Не думайте о функции, например о putStrLn, как о функции, которая принимает строку и печатает её на экране. Думайте о ней как о функции, которая принимает строку и возвращает действие ввода-вывода. Это действие при выполнении печатает нечто ценное на вашем терминале.
9
Больше ввода и вывода
Теперь, когда вы понимаете идеи, лежащие в основе ввода-вывода в языке Haskell, можно приступать к интересным штукам. В этой главе мы будем обрабатывать файлы, генерировать случайные числа, читать аргументы командной строки и много чего ещё. Будьте готовы!
Файлы и потоки
Вооружившись знанием того, как работают действия ввода-вывода, можно перейти к чтению и записи файлов. Но прежде давайте посмотрим, как Haskell умеет работать с потоками данных. Потоком называется последовательность фрагментов данных, которые поступают на вход программы и выводятся в результате её работы. Например, когда вы вводите в программу символы, печатая их на клавиатуре, последовательность этих символов может рассматриваться как поток.
Перенаправление ввода
Многие интерактивные программы получают пользовательский ввод с клавиатуры. Однако зачастую гораздо удобнее «скормить» программе содержимое текстового файла. Такой способ подачи входных данных называется перенаправлением ввода.
Посмотрим, как перенаправление ввода работает с программой на языке Haskell. Для начала создадим текстовый файл, содержащий небольшое хайку, и сохраним его под именем haiku.txt:
Я маленький чайник
Ох уж этот обед в самолёте
Он столь мал и невкусен
Ну да, хайку, прямо скажем, не шедевр – и что? Если кто в курсе, где найти хороший учебник по хайку, дайте знать.
Теперь напишем маленькую программу, которая непрерывно читает строку ввода и выводит её в верхнем регистре:
import Control.Monad
import Data.Char
main = forever $ do
l <- getLine
putStrLn $ map toUpper l
Сохраните эту программу в файле capslocker.hs и скомпилируйте её.
Вместо того чтобы вводить строки с клавиатуры, мы перенаправим на вход программы содержимое файла haiku.txt. Чтобы сделать это, нужно добавить символ < после имени программы и затем указать имя файла, в котором хранятся исходные данные. Посмотрите:
$ ghc capslocker
[1 of 1] Compiling Main ( capslocker.hs, capslocker.o )
Linking capslocker ...
$ ./capslocker < haiku.txt
Я МАЛЕНЬКИЙ ЧАЙНИК
ОХ УЖ ЭТОТ ОБЕД В САМОЛЁТЕ
ОН СТОЛЬ МАЛ И НЕВКУСЕН
capslocker: <stdin>: hGetLine: end of file
То, что мы проделали, практически эквивалентно запуску программы capslocker, вводу нашего хайку с клавиатуры и передаче символа конца файла (обычно это делается нажатием клавиш Ctrl+D). С тем же успехом можно было бы запустить capslocker и сказать: «Погоди, не читай ничего с клавиатуры, возьми содержимое этого файла!».
Получение строк из входного потока
Давайте посмотрим на действие ввода-вывода getContents, упрощающее обработку входного потока за счёт того, что оно позволяет рассматривать весь поток как обычную строку. Действие getContents читает всё содержимое стандартного потока ввода вплоть до обнаружения символа конца файла. Его тип: getContents :: IO String. Самое приятное в этом действии то, что ввод-вывод в его исполнении является ленивым. Это означает, что выполнение foo <- getContents не приводит к загрузке в память всего содержимого потока и связыванию его с именем foo. Нет, действие getContents для этого слишком лениво. Оно скажет: «Да, да, я прочту входные данные с терминала как-нибудь потом, когда это действительно понадобится!».
В примере capslocker.hs для чтения ввода строка за строкой и печати их в верхнем регистре использовалась функция forever. Если мы перейдём на getContents, то она возьмёт на себя все заботы о деталях ввода-вывода – о том, когда и какую часть входных данных нужно прочитать. Поскольку наша программа просто берёт входные данные, преобразует их и выводит результат, пользуясь getContents, её можно написать короче:
import Data.Char
main = do
contents <- getContents
putStr $ map toUpper contents
Мы выполняем действие getContents и даём имя contents строке, которую она прочтёт. Затем проходим функцией toUpper по всем символам этой строки и выводим результат на терминал. Имейте в виду: поскольку строки являются списками, а списки ленивы, как и действие getContents, программа не будет пытаться прочесть и сохранить в памяти всё содержимое входного потока. Вместо этого она будет читать данные порциями, переводить каждую порцию в верхний регистр и печатать результат.
Давайте проверим:
$ ./capslocker < haiku.txt
Я МАЛЕНЬКИЙ ЧАЙНИК
ОХ УЖ ЭТОТ ОБЕД В САМОЛЁТЕ
ОН СТОЛЬ МАЛ И НЕВКУСЕН
Работает. А что если мы просто запустим capslocker и будем печатать строки вручную (для выхода из программы нужно нажать Ctrl+D)?
$ ./capslocker
хей хо
ХЕЙ ХО
идём
ИДЁМ
Чудесно! Как видите, программа печатает строки в верхнем регистре по мере ввода строк. Когда результат действия getContents связывается с идентификатором сontents, он представляется в памяти не в виде настоящей строки, но в виде обещания, что рано или поздно он вернёт строку. Также есть обещание применить функцию toUpper ко всем символам строки сontents. Когда выполняется функция putStr, она говорит предыдущему обещанию: «Эй, мне нужна строка в верхнем регистре!». Поскольку никакой строки ещё нет, она говорит идентификатору сontents: «Аллё, а не считать ли строку с терминала?». Вот тогда функция getContents в самом деле считывает с терминала и передаёт строку коду, который её запрашивал, чтобы сделать что-нибудь осязаемое. Затем этот код применяет функцию toUpper к символам строки и отдаёт результат в функцию putStr, которая его печатает. После чего функция putStr говорит, «Ау, мне нужна следующая строка, шевелись!» – и так продолжается до тех пор, пока не закончатся строки на входе, что мы обозначаем символом конца файла.
Теперь давайте напишем программу, которая будет принимать некоторый вход и печатать только те строки, длина которых меньше 15 символов. Смотрим:
main = do
contents <- getContents
putStr $ shortLinesOnly contents
shortLinesOnly :: String -> String
shortLinesOnly = unlines . filter (line -> length line < 15) . lines
Фрагмент программы, ответственный за ввод-вывод, сделан настолько малым, насколько это вообще возможно. Так как предполагается, что наша программа печатает результат, основываясь на входных данных, её можно реализовать согласно следующей логике: читаем содержимое входного потока, запускаем на этом содержимом некоторую функцию, печатаем результат работы этой функции.
Функция shortLinesOnly принимает строку – например, такую: "короткоnдлииииииииииинноnкоротко". В этом примере в строке на самом деле три строки входных данных: две короткие и одна (посередине) длинная. В результате применения функции lines получаем список ["коротко", "длииииииииииинно", "коротко"]. Затем список строк фильтруется, и остаются только строки, длина которых меньше 15 символов: ["коротко", "коротко"]. Наконец, функция unlines соединяет элементы списка в одну строку, разделяя их символом перевода строки: "короткоnкоротко".
Попробуем проверить, что получилось. Сохраните этот текст в файле shortlines.txt:
Я короткая
И я
А я длиииииииинная!!!
А уж я-то какая длиннющая!!!!!!!
Коротенькая
Длиииииииииииииииииииииинная
Короткая
Сохраните программу в файле shortlinesonly.hs и скомпилируйте её:
$ ghc shortlinesonly.hs
[1 of 1] Compiling Main ( shortlinesonly.hs, shortlinesonly.o )
Linking shortlinesonly ...
Чтобы её протестировать, перенаправим содержимое файла shortlines.txt на её поток ввода:
$ ./shortlinesonly < shortlines.txt
Я короткая
И я
Коротенькая
Короткая
Видно, что на терминал выведены только короткие строки.
Преобразование входного потока
Подобная последовательность действий – считывание строки из потока ввода, преобразование её функцией и вывод результата – настолько часто встречается, что существует функция, которая делает эту задачу ещё легче; она называется interact. Функция interact принимает функцию типа String –> String как параметр и возвращает действие ввода-вывода, которое примет некоторый вход, запустит заданную функцию и распечатает результат. Давайте изменим нашу программу так, чтобы воспользоваться этой функцией:
- Программирование на языке Пролог для искусственного интеллекта - Иван Братко - Программирование
- От «Энигмы» до ChatGPT - Рустам Агамалиев - Программирование / Экономика
- Новое в зарплатном учете в 2023 году: лайфхаки бухгалтера в 1С - Компания СервисКлауд - Программирование / Финансы
- ДИАЛОГ С КОМПЬЮТЕРОМ - Александр Журавлев - Программирование
- Программист-фанатик - Чед Фаулер - Программирование
- Python для детей. Анимация с черепашьей графикой - Виктор Рабинович - Прочая детская литература / Программирование
- Убейте дракона! Как писать блестящие сценарии для видеоигр - Роберт Дентон Брайант - Программирование
- Советы по Delphi. Версия 1.4.3 от 1.1.2001 - Валентин Озеров - Программирование
- Хочу в геймдев! Основы игровой разработки для начинающих - Вячеслав Николаевич Уточкин - Программирование
- Эффективное использование STL - Скотт Мейерс - Программирование