ТОП 10:

Получение строк из входного потока



Давайте посмотрим на действие ввода-вывода 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 как параметр и возвраща- ет действие ввода-вывода, которое примет некоторый вход, запус- тит заданную функцию и распечатает результат. Давайте изменим нашу программу так, чтобы воспользоваться этой функцией:

main = interact shortLinesOnly

 

shortLinesOnly :: String -> String

 
 

shortLinesOnly = unlines . filter (\line -> length line < 15) . lines

Этой программой можно пользоваться, либо перенаправляя файл в поток ввода, либо вводя данные непосредственно с кла- виатуры, строка за строкой. Результат будет одинаковым, однако при вводе с клавиатуры входные данные будут чередоваться с вы- ходными.

Давайте напишем программу, которая постоянно считывает строку и затем говорит нам, является ли введённая строка палинд- ромом. Можно было бы использовать функцию getLine, чтобы она считывала строку, затем говорить пользователю, является ли она палиндромом, и снова запускать функцию main. Но легче делать это с помощью функции interact. Когда вы её используете, всегда ду- майте, как преобразовать некий вход в желаемый выход. В нашем случае мы хотим заменить строку на входе на "палиндром" или "не палиндром".


 

respondPalindromes :: String -> String respondPalindromes =

unlines .

map (\xs -> if isPal xs then "палиндром" else "не палиндром") . lines

 

 
 

isPal xs = xs == reverse xs

Всё вполне очевидно. Вначале преобразуем строку, например

 
 

"слон\nпотоп\nчто-нибудь"

в список строк

 
 

["слон", "потоп", "что-нибудь"]

Затем применяем анонимную функцию к элементам списка и по- лучаем:

 
 

["не палиндром", "палиндром", "не палиндром"]

Соединяем список обратно в строку функцией unlines. Теперь мы можем определить главное действие ввода-вывода:

 
 

main = interact respondPalindromes

Протестируем:

$ ./palindromes ха-ха

не палиндром арозаупаланалапуазора палиндром

печенька

 
 

не палиндром

Хоть мы и написали программу, которая преобразует одну боль- шую составную строку в другую составную строку, она работает так, как будто мы обрабатываем строку за строкой. Это потому что язык Haskell ленив – он хочет распечатать первую строку результата, но не может, поскольку пока не имеет первой строки ввода. Как толь- ко мы введем первую строку на вход, он напечатает первую строку на выходе. Мы выходим из программы по символу конца файла.


 

 
 

Также можно запустить нашу программу, перенаправив в неё со- держимое файла. Например, у нас есть файл words.txt:

кенгуру радар ротор мадам

 
 

Вот что мы получим, если перенаправим его на вход нашей про- граммы:

$ ./palindromes < words.txt не палиндром

 
 

палиндром палиндром палиндром

Ещё раз: результат аналогичен тому, как если бы мы запускали программу и вводили слова вручную. Здесь мы не видим входных строк, потому что вход берётся из файла, а не со стандартного ввода. К этому моменту, вероятно, вы уже усвоили, как работает лени- вый ввод-вывод и как его можно использовать с пользой для себя. Вы можете рассуждать о том, каким должен быть выход для дан- ного входа, и писать функцию для преобразования входа в выход. В ленивом вводе-выводе ничего не считывается со входа до тех пор, пока это не станет абсолютно необходимым для того, что мы

собираемся напечатать.

 

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

До сих пор мы работали с вводом-выводом, печатая на терминале и считывая с него. Ну а как читать и записывать файлы? В неко- тором смысле мы уже работали с файлами. Чтение с терминала можно представить как чтение из специального файла. То же вер- но и для печати на терминале – это почти что запись в файл. Два файла – 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 вы должны закрывать файлы самостоятельно!

 







Последнее изменение этой страницы: 2017-02-17; Нарушение авторского права страницы

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 35.172.216.157 (0.01 с.)