Многозадачный список задач
Начнём с реализации функции, которая принимает команду в виде строки (например, "add" или "view") и возвращает функцию, которая в свою очередь принимает список аргументов и возвращает действие ввода-вывода, выполняющее в точности то, что необходимо:
import System.Environment
import System.Directory
import System.IO
import Data.List
import Control.Exception
dispatch :: String -> [String] –> IO ()
dispatch "add" = add
dispatch "view" = view
dispatch "remove" = remove
Функция main будет выглядеть так:
main = do
(command:argList) <- getArgs
dispatch command argList
Первым делом мы получаем аргументы и связываем их со списком (command:argsList). Таким образом, первый аргумент будет связан с именем command, а все остальные – со списком argList. В следующей строке к переменной commands применяется функция dispatch, результатом которой может быть одна из функций add, view или remove. Затем результирующая функция применяется к списку аргументов argList.
Предположим, программа запущена со следующими параметрами:
$ ./todo add todo.txt "Найти магический меч силы"
Тогда значением command будет "add", а значением argList – список ["todo.txt", "Найти магический меч силы"]. Поэтому сработает первый вариант определения функции dispatch и будет возвращена функция add. Применяем её к argList, результатом оказывается действие ввода-вывода, добавляющее новое задание в список.
Теперь давайте реализуем функции add, view и remove. Начнём с первой из них:
add :: [String] –> IO ()
add [fileName, todoItem] = appendFile fileName (todoItem ++ " ")
При вызове
$ ./todo add todo.txt "Найти магический меч силы"
функции add будет передан список ["todo.txt", "Найти магический меч силы"]. Поскольку пока мы не обрабатываем некорректный ввод, достаточно будет сопоставить аргумент функции add с двухэлементным списком. Результатом функции будет действие ввода-вывода, добавляющее строку вместе с символом конца строки в конец файла.
Далее реализуем функциональность просмотра списка. Если мы хотим просмотреть элементы списка, то вызываем программу так: todo view todo.txt. В первом сопоставлении с образцом идентификатор command будет связан со строкой view, а идентификатор argList будет равен ["todo.txt"].
Вот код функции view:
view :: [String] –> IO ()
view [fileName] = do
contents <– readFile fileName
let todoTasks = lines contents
numberedTasks = zipWith ( line –> show n ++ " – " ++ line)
[0..] todoTasks
putStr $ unlines numberedTasks
Программа, которая удаляла задачу из списка, производила практически те же самые действия: мы отображали список задач, чтобы пользователь мог выбрать, какую из них удалить. Но в этой функции мы просто отображаем список.
Ну и наконец реализуем функцию remove. Функция будет очень похожа на программу для удаления элемента, так что если вы не понимаете, как работает функция удаления, прочитайте пояснения к её определению. Основное отличие – мы не задаём жёстко имя файла, а получаем его как аргумент. Также мы не спрашиваем у пользователя номер задачи для удаления – его мы также получаем в виде аргумента.
remove :: [String] -> IO ()
remove [fileName, numberString] = do
contents <- readFile fileName
let todoTasks = lines contents
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 fileName
renameFile tempName fileName)
Мы открываем файл, полное имя которого задаётся в идентификаторе fileName, открываем временный файл, удаляем строку по индексу, записываем во временный файл, удаляем исходный файл и переименовываем временный в fileName. Приведём полный листинг программы во всей её красе:
import System.Environment
import System.Directory
import System.IO
import Control.Exception
import Data.List
dispatch :: String -> [String] -> IO ()
dispatch "add" = add
dispatch "view" = view
dispatch "remove" = remove
main = do
(command:argList) <- getArgs
dispatch command argList
add :: [String] -> IO ()
add [fileName, todoItem] = appendFile fileName (todoItem ++ " ")
view :: [String] -> IO ()
view [fileName] = do
contents <- readFile fileName
let todoTasks = lines contents
numberedTasks = zipWith ( line -> show n ++ " – " ++ line)
[0..] todoTasks
putStr $ unlines numberedTasks
remove :: [String] -> IO ()
remove [fileName, numberString] = do
contents <- readFile fileName
let todoTasks = lines contents
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 fileName
renameFile tempName fileName)
Резюмируем наше решение. Мы написали функцию dispatch, отображающую команды на функции, которые принимают аргументы командной строки в виде списка и возвращают соответствующее действие ввода-вывода. Основываясь на значении первого аргумента, функция dispatch даёт нам необходимую функцию. В результате вызова этой функции мы получаем требуемое действие и выполняем его.
Давайте проверим, как наша программа работает:
$ ./todo view todo.txt
0 – Погладить посуду
1 – Помыть собаку
2 – Вынуть салат из печи
$ ./todo add todo.txt "Забрать детей из химчистки"
$ ./todo view todo.txt
0 – Погладить посуду
1 – Помыть собаку
2 – Вынуть салат из печи
3 – Забрать детей из химчистки
$ ./todo remove todo.txt 2
$ ./todo view todo.txt
0 – Погладить посуду
1 – Помыть собаку
2 – Забрать детей из химчистки
Большой плюс такого подхода – легко добавлять новую функциональность. Добавить вариант определения функции dispatch, реализовать соответствующую функцию – и готово! В качестве упражнения можете реализовать функцию bump, которая примет файл и номер задачи и вернёт действие ввода-вывода, которое поднимет указанную задачу на вершину списка задач.