["Погладить посуду", "Помыть собаку", "Вынуть салат из печи"]
Далее соединяем числа, начиная с 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