["Погладить посуду", "Помыть собаку", "Вынуть салат из печи"]
Далее соединяем числа, начиная с 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
плюс несколько случайных символов. Функция возвращает действие ввода-вывода, которое создаст временный файл; результат действия – пара значений, имя временного файла и дескриптор. Мы могли бы открыть обычный файл, например с именем openTempFile
– хорошая практика: в этом случае не приходится опасаться, что вы случайно что-нибудь перезапишете.
Теперь, когда временный файл открыт, запишем туда строку newTodoItems
. В этот момент исходный файл не изменён, а временный содержит все строки из исходного, за исключением удалённой.
Затем мы закрываем временный файл и удаляем исходный с помощью функции removeFile
, которая принимает путь к файлу и удаляет его. После удаления старого файла renameFile
, чтобы переименовать временный файл в removeFile
и renameFile
(обе они определены в модуле System.Directory
) принимают в качестве параметров не дескрипторы, а пути к файлам.
Сохраните программу в файле с именем
$ ./deletetodo
Ваши задания:
0 – Погладить посуду
1 – Помыть собаку
2 – Вынуть салат из печи
Что вы хотите удалить?
1
Смотрим, что осталось:
$ cat todo.txt
Погладить посуду
Вынуть салат из печи
Круто! Удалим ещё что-нибудь:
$ ./deletetodo
Ваши задания:
0 – Погладить посуду
1 – Вынуть салат из печи
Что вы хотите удалить?
0
Проверяя файл с заданиями, убеждаемся, что осталось только одно:
$ cat todo.txt
Вынуть салат из печи
Итак, всё работает. Осталась только одна вещь, которую мы в этой программе не учли. Если после открытия временного файла что-то произойдёт и программа неожиданно завершится, то временный файл не будет удалён. Давайте это исправим.
Уборка
Чтобы гарантировать удаление временного файла, воспользуемся функцией bracketOnError
из модуля Control.Exception
. Она очень похожа на bracket
, но если последняя получает ресурс и гарантирует, что освобождение ресурса будет выполнено всегда, то функция bracketOnError
выполнит завершающие действия только в случае возникновения исключения. Вот исправленный код:
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