ТОП 10:

Использование функции withFile



 
 

То, что мы только что сделали, можно сделать и по-другому – с ис- пользованием функции withFile. Сигнатура этой функции:

withFile :: FilePath –> IOMode –> (Handle –> IO a) –> IO a

Она принимает путь к файлу, режим открытия файла и некото- рую функцию, принимающую дескриптор и возвращающую некое действие ввода-вывода. Функция withFile вернёт действие ввода- вывода, которое откроет файл, сделает с ним то, что нам нужно, и закроет его. Результат, помещённый в заключительном действии ввода-вывода, будет взят из результата переданной нами функции. С виду это может показаться сложным, но на самом деле всё просто, особенно если использовать анонимные функции. Вот как можно пе- реписать предыдущий пример с использованием функции withFile:

import System.IO main = do

withFile "girlfriend.txt" ReadMode (\handle –> do contents <– hGetContents handle

 
 

putStr contents)


 

Функция (\handle -> …) принимает дескриптор файла и возвра- щает действие ввода-вывода. Обычно пишут именно так, пользу- ясь анонимной функцией. Нам действительно нужна функция, возвращающая действие ввода-вывода, а не просто выполнение некоторого действия и последующее закрытие файла, посколь- ку действие, переданное функции withFile, не знало бы, с каким файлом ему необходимо работать. Сейчас же функция withFile открывает файл, а затем передаёт его дескриптор функции, кото- рую мы ей передали. Функция возвращает действие ввода-вывода, на основе которого withFile создаёт новое действие, работающее почти так же, как и исходное, но с добавлением гарантированно- го закрытия файла даже в тех случаях, когда что-то пошло не так.

 

Время заключать в скобки

Обычно, если какой-нибудь фрагмент кода вызывает функцию

error (например, когда мы пытаемся вызвать функцию head для

пустого списка) или случается что-то плохое при вводе-выво- де, наша программа завершается с сообщениемобошибке. Втаких обстоятельствах говорят, что произошло исключение. Функция withFile гарантирует, что незави- симо от того, возникнет исключе- ние или нет, файл будет закрыт. Подобные сценарии встреча- ются довольно часто. Мы получа- ем в распоряжение некоторый ресурс (например, файловый дескриптор), хотим с ним что-ни- будь сделать, но кроме того хотим, чтобы он был освобождён (файл закрыт). Как раз для таких слу- чаев в модуле Control.Exception имеется функция bracket. Вот её

 
 

сигнатура:

bracket :: IO a -> (a -> IO b) -> (a -> IO c) -> IO c


 

Первым параметром является действие, получающее ресурс (дескриптор файла). Второй параметр – функция, освобождающая ресурс. Эта функция будет вызвана даже в случае возникновения исключения. Третий параметр – это функция, которая также при- нимает на вход ресурс и что-то с ним делает. Именно в третьем па- раметре и происходит всё самое важное, а именно: чтение файла или его запись.

 
 

Поскольку функция bracket – это и есть всё необходимое для получения ресурса, работы с ним и гарантированного освобожде- ния, с её помощью можно получить простую реализацию функции withFile:

withFile :: FilePath –> IOMode –> (Handle –> IO a) –> IO a withFile name mode f = bracket (openFile name mode)

 
 

(\handle -> hClose handle) (\handle -> f handle)

Первый параметр, который мы передали функции bracket, от- крывает файл; результатом являетсядескриптор. Второйпараметр принимает дескриптор и закрывает его. Функция bracket даёт га- рантию, что это произойдёт, даже если возникнет исключение. На- конец, третий параметр функции bracket принимает дескриптор и применяет к нему функцию f, которая по заданному дескриптору делает с файлом всё необходимое, будь то его чтение или запись.

 

Хватай дескрипторы!

Подобно тому как функция hGetContents работает по аналогии с функцией getContents, но с указанным файлом, существуют фун- кции hGetLine, hPutStr, hPutStrLn, hGetChar и т. д., ведущие себя так же, как их варианты без буквы h, но принимающие дескриптор как параметр и работающие с файлом, а не со стандартным вводом- выводом. Пример: putStrLn – это функция, принимающая строку и возвращающая действие ввода-вывода, которое напечатает стро- ку на терминале, а затем выполнит перевод на новую строку. Функ- ция hPutStrLn принимает дескриптор файла и строку и возвращает действие, которое запишет строку в файл и затем поместит в файл символ(ы) перехода на новую строку. Функция hGetLine принимает дескриптор и возвращает действие, которое считывает строку из файла.


 

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

 
 

Сигнатура функции readFile такова:

readFile :: FilePath –> IO String

 
 

Мы помним, что тип FilePath – это просто удобное обозначение для String. Функция readFile принимает путь к файлу и возвращает действие ввода-вывода, которое прочитает файл (лениво, конечно же) и свяжет содержимое файла в виде строки с некоторым именем. Обычно это более удобно, чем вызывать функцию openFile и связы- вать дескриптор с именем, а затем вызывать функцию hGetContents. Вот как мы могли бы переписать предыдущий пример с использо- ванием readFile:

import System.IO

 

main = do

 
 

contents <– readFile "girlfriend.txt" putStr contents

Так как мы не получаем дескриптор файла в качестве результа- та, то не можем закрыть его сами. Если мы используем функцию readFile, за нас это сделает язык Haskell.

 
 

Функция writeFile имеет тип

writeFile :: FilePath –> String –> IO ()

 
 

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

import System.IO import Data.Char

 

main = do

contents <– readFile "girlfriend.txt"

 
 

writeFile "girlfriendcaps.txt" (map toUpper contents)


 

Функция appendFile имеет ту же сигнатуру, что и writeFile, и дейс- твует почти так же. Она только не обрезает уже существующий файл до нулевой длины перед записью, а добавляет новое содержимое в конец файла.

 

Список дел

Воспользуемся функцией appendFile на примере написания про- граммы, которая добавляет в текстовый файл, содержащий список наших дел, новое задание. Допустим, у нас уже есть такой файл с на- званием todo.txt, и каждая его строка соответствует одному заданию.

 
 

Наша программа будет читать из стандартного потока ввода одну строку и добавлять её в конец файла todo.txt:

import System.IO

 

main = do

todoItem <– getLine

 
 

appendFile "todo.txt" (todoItem ++ "\n")

Обратите внимание на добавление символа конца строки вруч- ную, функция getLine возвращает строку без него.

 
 

Сохраните этот файл с именем appendtodo.hs, скомпилируйте его и несколько раз запустите.

$ ./appendtodo Погладить посуду

$ ./appendtodo Помыть собаку

$ ./appendtodo Вынуть салат из печи

$ cat todo.txt Погладить посуду Помыть собаку

 
 

Вынуть салат из печи

 

ПРИМЕЧАНИЕ.Программа cat в Unix-подобных системах исполь- зуется для вывода содержимого текстового файла на терминал. В Windows можно воспользоваться командой type или посмотреть содержимое файла в любом текстовом редакторе.


Удаление заданий

 
 

Мы уже написали программу, которая добавляет новый элемент к списку заданий в файл todo.txt; теперь напишем программу для уда- ления элемента. Мы применим несколько новых функций из моду- ля System.Directory и одну новую функцию из модуля System.IO; их работа будет объяснена позднее.

import System.IO

import System.Directory import Data.List

 

main = do

contents <– readFile "todo.txt" let todoTasks = lines contents

numberedTasks = zipWith (\n line –> show n ++ " – " ++ line)

[0..] todoTasks

putStrLn "Ваши задания:" mapM_ putStrLn numberedTasks

putStrLn "Что вы хотите удалить?" numberString <– getLine

let number = read numberString

newTodoItems = unlines $ delete (todoTasks !! number) todoTasks (tempName, tempHandle) <– openTempFile "." "temp"

hPutStr tempHandle newTodoItems hClose tempHandle

 
 

removeFile "todo.txt" renameFile tempName "todo.txt"

Сначала мы читаем содержимое файла todo.txt и связываем его с именем contents. Затем разбиваем всё содержимое на список строк. Список todoTasks выглядит примерно так:

 
 

["Погладить посуду", "Помыть собаку", "Вынуть салат из печи"]

Далее соединяем числа, начиная с 0, и элементы списка дел с помощью функции, которая берёт число (скажем, 3) и строку (на- пример, "привет") и возвращает новую строку ("3 – привет"). Вот примерный вид списка numberedTasks:

 
 

["0 - Погладить посуду", "1 - Помыть собаку", "2 - Вынуть салат из печи"]


 

Затем с помощью вызова mapM_ putStrLn numberedTasks мы печа- таем каждое задание на отдельной строке, после чего спрашиваем пользователя, что он хочет удалить, и ждём его ответа. Например, он хочет удалить задание 1 (Помыть собаку), так что мы получим число 1. Значением переменной numberString будет "1", и, посколь- ку вместо строки нам необходимо число, мы применяем функцию read и связываем результат с именем number.

Помните функции delete и !! из модуля Data.List? Оператор !! возвращает элемент из списка по индексу, функция delete удаляет первое вхождение элемента в список, возвращая новый список без удалённого элемента. Выражение (todoTasks !! number), где number – это 1, возвращает строку "Помыть собаку". Мы удаляем первое вхож- дение этой строки из списка todoTasks, собираем всё оставшееся в одну строку функцией unlines и даём результату имя newTodoItems. Далее используем новую функцию из модуля System.IO – openTempFile. Имя функции говорит само за себя: open temp file –

«открыть временный файл». Она принимает путь к временному каталогу и шаблон имени файла и открывает временный файл. Мы использовали символ . в качестве каталога для временных файлов, так как . обозначает текущий каталог практически во всех операци- онных системах. Строку "temp" мы указали в качестве шаблона имени для временного файла; это означает, что временный файл будет на- зван temp плюс несколько случайных символов. Функция возвращает действие ввода-вывода, которое создаст временный файл; результат действия – пара значений, имя временного файла и дескриптор. Мы могли бы открыть обычный файл, например с именем todo2.txt, но использовать openTempFile – хорошая практика: в этом случае не при- ходится опасаться, что вы случайно что-нибудь перезапишете.

Теперь, когда временный файл открыт, запишем туда строку newTodoItems. В этот момент исходный файл не изменён, а времен- ный содержит все строки из исходного, за исключением удалённой.

Затем мы закрываем временный файл и удаляем исходный с помощью функции removeFile, которая принимает путь к файлу и удаляет его. После удаления старого файла todo.txt мы использу- ем функцию renameFile, чтобы переименовать временный файл в todo.txt. Обратите внимание: функции removeFile и renameFile (обе они определены в модуле System.Directory) принимают в качестве параметров не дескрипторы, а пути к файлам.


 

 
 

Сохраните программу в файле с именем deletetodo.hs, скомпили- руйте её и проверьте:

$ ./deletetodo Ваши задания:

0 – Погладить посуду 1 – Помыть собаку

2 – Вынуть салат из печи Что вы хотите удалить?

 
 

1

Смотрим, что осталось:

 
 

$ cat todo.txt Погладить посуду Вынуть салат из печи

Круто! Удалим ещё что-нибудь:

$ ./deletetodo Ваши задания:

0 – Погладить посуду

1 – Вынуть салат из печи Что вы хотите удалить?

 
 

0

Проверяя файл с заданиями, убеждаемся, что осталось только одно:

 
 

$ cat todo.txt Вынуть салат из печи

Итак, всё работает. Осталась только одна вещь, которую мы в этой программе не учли. Если после открытия временного файла что-то произойдёт и программа неожиданно завершится, то вре- менный файл не будет удалён. Давайте это исправим.

 

Уборка

Чтобы гарантировать удаление временного файла, воспользуемся функцией bracketOnError из модуля Control.Exception. Она очень по- хожа на bracket, но если последняя получает ресурс и гарантирует, что освобождение ресурса будет выполнено всегда, то функция bracketOnError выполнит завершающие действия только в случае возникновения исключения. Вот исправленный код:


АРГУМЕНТЫ КОМАНДНОЙ СТРОКИ 241

 
 

import System.IO

import System.Directory import Data.List

import Control.Exception

 

main = do

contents <– readFile "todo.txt" let todoTasks = lines contents

numberedTasks = zipWith (\n line –> show n ++ " – " ++ line)

[0..] todoTasks

putStrLn "Ваши задания:" mapM_ putStrLn numberedTasks

putStrLn "Что вы хотите удалить?" numberString <– getLine

let number = read numberString

newTodoItems = unlines $ delete (todoTasks !! number) todoTasks bracketOnError (openTempFile "." "temp")

(\(tempName, tempHandle) –> do hClose tempHandle removeFile tempName)

(\(tempName, tempHandle) –> do hPutStr tempHandle newTodoItems hClose tempHandle

 
 

removeFile "todo.txt" renameFile tempName "todo.txt")

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

 

Аргументы командной строки

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

В предыдущей главе мы написали программы для добавления и удаления элемента в список заданий. Но у нашего подхода есть две


 

проблемы. Во-первых, мы жёстко задали имя файла со списком заданий в текс- те программы. Мы решили, что файл будет называться todo.txt, и что пользователь никогда не захочет вести несколько списков.

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

списком заданий. Мы использовали такой подход, когда спраши- вали пользователя, какой элемент он хочет удалить. Это, конеч- но, работает, но не идеально, поскольку пользователь должен запустить программу, подождать, пока она спросит что-нибудь, и затем дать ответ. Такая программа называется интерактивной, и сложность здесь заключается вот в чём: вдруг вам понадобится автоматизировать выполнение этой программы, например, с по- мощью скрипта? Гораздо сложнее написать скрипт, который будет взаимодействовать с программой, чем обычный скрипт, который просто вызовет её один или несколько раз!

Вот почему иногда лучше сделать так, чтобы пользователь сооб- щал, чего он хочет, при запуске программы, вместо того чтобы она сама спрашивала его после запуска. И что может послужить этой цели лучше командной строки!..

В модуле System.Environment есть два полезных действия ввода- вывода. Первое – это функция getArgs; её тип – getArgs :: IO [String]. Она получает аргументы, с которыми была вызвана программа, и возвращает их в виде списка. Второе – функция getProgName, тип которой – getProgName :: IO String. Это действие ввода-вывода, воз- вращающее имя программы.

 
 

Вот простенькая программа, которая показывает, как работают эти два действия:

import System.Environment import Data.List

 

main = do


 

args <– getArgs progName <– getProgName

putStrLn "Аргументы командной строки:" mapM putStrLn args

 
 

putStrLn "Имя программы:" putStrLn progName

Мы связываем значения, возвращаемые функциями getArgs и progName, с именами args и progName. Выводим строку "Аргументы командной строки:" и затем для каждого аргумента из списка args вы- полняем функцию putStrLn. После этого печатаем имя программы. Скомпилируем программу с именем arg-test и проверим, как она работает:

$ ./arg-test first second w00t "multi word arg" Аргументы командной строки:

first second w00t

 
 

multi word arg Имя программы: arg-test

 







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

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