<
  • Главная
Статьи

Проста консольна програма для ведення списку завдань (todo)

У цій статті я розповім про те, як одного разу я написав невелику консольную програму для ведення списку завдань майже повністю в функціональному стилі (за винятком використання змінних і деяких прийомів з ООП).

На жаль, в статті крім корисних практичних прийомів будуть і деякі негативні приклади, зокрема, деяке дублювання коду і нераціональне використання функцій стандартної бібліотеки.

Також в цій скромній публікації я покажу з чого я почав функціональне проектування програми (буде показаний приклад коду на одному з функціональних мов), нестандартне використання одного файлу зі складу бібліотеки QtE5, одну цікаву бібліотеку для розфарбовування повідомлень у командному рядку, а також я зроблю невеликий резюме про отриманому в ході роботи над цією програмою todo досвіді.

Ідея написати подібний додаток прийшла мені в голову після прочитання чудової книги Мірана Ліповача «Вивчай Haskell в ім'я добра!» , Яку я почав читати просто заради інтересу і пізнання дао функціонального програмування. У цьому Нескучне підручнику наводився вихідний код програми для ведення списку завдань (далі для стислості, я буду називати список завдань словом todo) на Haskell і пропонувалося в якості вправи написати кілька функцій, неописаних автором. Що сказати, мені кинули виклик.

Суть програми гранично проста. Є звичайний текстовий файл (розширення - * .txt, хоча це і не принципово) і в ньому зберігається набір записів, розділених новим рядком. Програма має ряд команд add, remove, view, bump за допомогою яких користувач може додавати, видаляти, переглядати і піднімати на вершину списку записи з файлу. При цьому всі команди віддаються виключно з командного рядка.

Cінтаксіс команд дуже простий:

todo add & lt; ім'я файлу & gt; & Lt; запісь_1 & gt; & Lt; запісь_2 & gt; & Lt; запісь_3 & gt; ... & lt; запісь_N & gt; todo remove & lt; ім'я файлу & gt; & Lt; номер запису & gt; todo view & lt; ім'я файлу & gt; todo bump & lt; ім'я файлу & gt; & Lt; номер запису & gt;

На Haskell програма, яка реалізує всі ці команди, з урахуванням можливого некоректного введення і з рядом деяких моїх правок виглядає приблизно так (не питайте мене, коли і як я вчив мову):

import Control.Exception import Data.List import System.Directory import System.Environment import System.IO - Доступні команди dispatch :: String - & gt; [String] - & gt; IO () dispatch command | command == "add" = add | command == "view" = view | command == "remove" = remove | command == "bump" = bump | otherwise = doesntExist command - Обробка неправильної команди doesntExist :: String - & gt; [String] - & gt; IO () doesntExist command _ = if command == "" then putStrLn "Empty command!" else putStrLn $ "Command" ++ command ++ "is not exist" --Программа діє тільки, якщо файл дійсно існує withCorrectFile :: String - & gt; IO () - & gt; IO () withCorrectFile fileName fileAction = do fileExists & lt; - doesFileExist fileName if fileExists then fileAction else putStrLn $ "File" ++ fileName ++ "does not exists!" - Додати завдання в список завдань add :: [String] - & gt; IO () add [fileName, todoItem] = appendFile fileName (todoItem ++ "\ n") add _ = putStrLn "Command add has exactly two arguments" - Переглянути завдання з поточного списку view :: [String] - & gt; IO () view [fileName] = withCorrectFile fileName (do contents & lt; - readFile fileName let todoTasks = lines contents numberedTasks = zipWith (\ n line - & gt; show n ++ "-" ++ line) [0 ..] todoTasks putStr $ unlines numberedTasks) view _ = putStrLn "Command view has exactly one argument" - Допоміжна функція для маніпулювання файлами - потрібна в тому випадку, якщо файл оновлюється fileManipulate :: String - & gt; String - & gt; IO () fileManipulate fileName todoItems = withCorrectFile fileName (bracketOnError (openTempFile "." "Temp") (\ (temporaryFileName, temporaryFile) - & gt; do hClose temporaryFile removeFile temporaryFileName) (\ (temporaryFileName, temporaryFile) - & gt; do hPutStr temporaryFile todoItems hClose temporaryFile removeFile fileName renameFile temporaryFileName fileName)) - Видалення завдання зі списку remove :: [String] - & gt; IO () remove [fileName, numberOfString] = do contents & lt; - readFile fileName let todoTasks = lines contents number = read numberOfString todoItems = unlines $ delete (todoTasks !! number) todoTasks fileManipulate fileName todoItems remove _ = putStrLn "Command remove has exactly two arguments "- Підняти завдання на верх списку завдань bump :: [String] - & gt; IO () bump [fileName, numberOfString] = do contents & lt; - readFile fileName let todoTasks = lines contents number = read numberOfString todo = todoTasks !! number todoItems = unlines $ (todo: (delete todo todoTasks)) fileManipulate fileName todoItems bump _ = putStrLn "Command bump has exactly two arguments" main :: IO () main = do arguments & lt; - getArgs if length arguments == 0 then putStrLn "Usage: todo & lt; add | remove | view | bump & gt; [arguments]" else do (command: argumentsList) & lt; - getArgs dispatch command argumentsList

89 рядків майже чистого функціонального коду, і при цьому, я впевнений, що це ще можна оптимізувати і поліпшити! Haskell дає дуже цінний урок: багато програм можуть бути написані без використання циклів, змінних і деяких інших речей, при цьому програмний код стає більш якісним і більш простим в супроводі і тестуванні.

Саме з цієї причини, я рекомендую читачеві познайомитися з цією мовою програмування, щоб зрозуміти концепції функціонального програмування і навчитися функціональному мислення.

Однак, повернемося в D.

Для інтересу, я змінив деякі команди в додатку (ну і дещо додав) і зробив кольоровий висновок на екран. Команду bump я замінив на команду head, а також ввів команду tail, яка по синтаксисі збігається з bump, але абсолютно протилежна по результату дії (tail переносить завдання в самий низ списку завдань).

Для роботи над програмою нам буде потрібно викачані бібліотеки QtE5 і arsd , А саме, файли asc1251.d (з QtE5) і terminal.d (з arsd): asc1251 містить набір процедур, які вміють працювати з кодуванням в командному рядку Windows, а terminal.d - містить набір процедур, для роботи з командним рядком .

Ідея, яку я буду використовувати в своєму додатку приблизно наступна: програма todo отримує з консолі команди маніпуляції списком завдань, розбирає їх, виводячи відповідні повідомлення користувачу (враховує і коректний і некоректний введення з боку користувача), і передає ім'я команди управління todo і її аргументи в певний виконавець, який і викличе цікаву для нас процедуру.

Все сказане описується так:

import std.algorithm; import std.conv; import std.file; import std.path; import std.range; import std.stdio; import std.string; import asc1251; import terminal; void main (string [] arguments) {auto parsedArguments = arguments.drop (1); auto terminal = Terminal (ConsoleOutputType.linear); if (parsedArguments.empty) {terminal.color (Color.red, Color.black); terminal.writeln ( "\ nNot enough arguments!"); } Else {auto command = parsedArguments.front; auto commandArguments = parsedArguments.drop (1) .array; executeCommand (terminal, command, commandArguments); }}

Що тут відбувається? Аргумент arguments процедури main містить в собі список всіх рядків переданих в командному рядку з додатком плюс ім'я самого додатка, тому за допомогою алгоритму drop ми позбавляємося від нульового елемента масиву arguments (drop повертає діапазон, який виходить шляхом пропуску n перших елементів переданого в неї діапазону). Далі створюємо структуру, через яку будемо маніпулювати терміналом і поміщаємо її в змінну terminal.

Якщо, список аргументів, оброблений drop, виявляється порожнім, то це означає, що програмі в командному рядку не були передані аргументи. У цьому випадку, ми встановлюємо в якості фонового кольору командного рядка чорний, а в якості кольору повідомлення - червоний, використовуючи метод color і ряд описаних в arsd типів Color.red і Color.black. Саме повідомлення виводиться в командний рядок за допомогою методу writeln, який аналогічний функції writeln з std.stdio. Таким чином, в разі запуску програми todo без аргументів, користувачеві червоним шрифтом буде виведена напис «Not enough arguments!» ( «Недостатньо аргументів!»).

Якщо, список аргументів після drop виявився не порожнім, то в змінну command за допомогою методу front виділяємо перший елемент обробленого списку, а в commandArguments - за допомогою drop поміщаємо аргументи команди маніпуляції todo. Далі за допомогою алгоритму array ми переводимо діапазон в масив, який поряд з іншими аргументами (структура терміналу і сама команда) передається в виконавець executeCommand.

Виконавець виглядає досить просто:

void executeCommand (ref Terminal terminal, string command, string [] arguments) {switch (command.toLower.strip) {case "add": addTodo (terminal, arguments); break; case "view": viewTodo (terminal, arguments); break; case "remove": removeTodo (terminal, arguments); break; case "head": moveTodoUp (terminal, arguments); break; case "tail": moveTodoDown (terminal, arguments); break; case "": terminal.color (Color.red, Color.black); terminal.writeln ( "\ nEmpty command!"); break; default: with (terminal) {terminal.color (Color.red, Color.black); terminal.write ( "\ nUnknown command"); terminal.color (Color.yellow, Color.black); terminal.write (command); terminal.color (Color.red, Color.black); terminal.writeln ( "!"); } Break; }}

Досить простий код, який дозволяє вибрати потрібну функцію для виконання, а також дозволяє правильно обробити ситуації, коли команда маніпуляції todo є порожній рядок або невідому команду. При цьому, перед попаданням в виконавець рядок наводиться до нижнього регістру (toLower) і з неї вирізують кінцеві і початкові пробіли (strip).

Тепер залишається реалізувати окремі функції, які будуть виконувати всі дії команд маніпуляції списком завдань.

Функція addTodo виглядає наступним чином:

void addTodo (ref Terminal terminal, string [] arguments) {if (arguments.length & lt; 2) {terminal.color (Color.red, Color.black); terminal.writeln ( "\ nCommand \" add \ "has 2 arguments"); } Else {auto fileName = arguments.front; File file; file.open (fileName, "a +"); arguments .drop (1) .filter! (a = & gt; (a! = "")? true: false) .map! (a = & gt; toCON (a)) .each! (a = & gt; file.writeln (a)); auto numberOfTodo = arguments.drop (1) .length; terminal.color (Color.green, Color.black); terminal.writefln ( "\ n% d" ~ "todo (s) was been added in file% s.", numberOfTodo, fileName); }}

Працює це таким чином: якщо довжина аргументів команди add менше 2, то значить, що користувач десь помилився і йому буде виведено повідомлення «Command add has 2 arguments» ( «Команда add має 2 аргументу»), в іншому випадку - переданий список аргументів add містить ім'я файлу для обробки і список записів для внесення в файл. Після вилучення імені файлу відбувається його відкриття в режимі додавання даних, після чого йде деяка хитра обробка вмісту arguments.

Спочатку з arguments видаляється перший елемент (drop), після чого виділяються тільки непусті рядки (за допомогою алгоритму filter і анонімної функції a => (a! = "")? True: false, яка описує умову фільтрації), проводиться переклад в кодування консолі (toCON з asc1251.d) і відповідно запис результату в файл (за допомогою алгоритму each і анонімної функції a => file.writeln (a)).

Після записування всіх записів в файл, ми підраховуємо їх кількість, робимо колір шрифту в командному рядку зеленим, і виводимо повідомлення англійською про те, що певна кількість записів було додано в деякий файл.

Функція removeTodo виглядає так і має кілька паралелей з уже розглядалася addTodo:

void removeTodo (ref Terminal terminal, string [] arguments) {if (arguments.length & lt; 2) {terminal.color (Color.red, Color.black); terminal.writeln ( "\ nCommand \" remove \ "has 2 arguments"); } Else {auto fileName = arguments.front; if (fileName.exists) {auto contents = (cast (string) std.file.read (fileName)) .splitLines; try {int index = to! size_t (arguments [1] .strip); File temporaryFile; temporaryFile.open (fileName ~ `.temp`,` w`); contents .removeNth (index) .each! (a = & gt; temporaryFile.writeln (a)); temporaryFile.close; remove (fileName); rename (fileName ~ `.temp`, fileName); terminal.color (Color.green, Color.black); terminal.writefln ( "\ nTodo with number% d was been removed from file% s.", index, fileName); } Catch (Exception e) {terminal.color (Color.red, Color.black); terminal.writeln ( "\ nSecond argument must be a positive integer!"); }} Else {terminal.color (Color.red, Color.black); terminal.writeln ( "\ nFile" ~ fileName ~ "does not exists!"); }}}

Також, як і в попередній функції, перевіряється довжина списку переданих аргументів, і в разі якщо вона менше 2, то видається попередження; в іншому випадку - відбувається подальша обробка аргументів. Функція removeTodo вважає, що перший переданий їй аргумент - це ім'я файлу, а другий - номер запису в файлі (до номерів записів файлу я ще повернуся). У програмі цей факт використовується на всю котушку: витягуючи ім'я файлу зі списку аргументів, тут же перевіряється його існування (exists зі стандартної бібліотеки) і відразу ж проводиться витяг з подальшим приведенням до size_t (попередньо з рядка, що містить другий аргумент, вирізаються зайві терминирующего символи : прогалини і їм подібні). Якщо потрібний файл не існує, то буде виведено повідомлення «File does not exists!» (Файл не існує). Якщо з якихось причин не вдалося виконати перетворення, то виникне виняток, яке буде перехоплено за допомогою try / catch блоку і користувач побачить повідомлення «Second argument must be a positive integer!» (Другий аргумент повинен бути позитивним цілим).

Якщо все передані аргументи коректні, то для здійснення видалення запису з файлу необхідно вважати весь файл в масив рядків, видалити з цього масиву елемент з потрібним індексом (removeNth), перенести масив рядків в тимчасовий файл (each і writeln), видалити вихідний файл (remove ) і перейменувати тимчасовий файл, використовуючи ім'я вихідного файлу (rename). Саме це і відбувається всередині блоку обробки виключення, в якому для видалення елемента з масиву використовується допоміжна функція removeNth, яка описується в такий спосіб:

T [] removeNth (T, U) (T [] array, U index) {auto newIndex = cast (size_t) index; if (array.length & lt; 0) {return array; } Else {if (newIndex & lt; array.length) {return array [0..newIndex] ~ array [newIndex + 1 .. $]; }} Return array; }

У разі успішного видалення запису програма видасть написане зеленим кольором повідомлення «Todo with number% d was been removed from file% s." (Запис з номером% d була видалена з файлу% s), яке легко і просто формується за допомогою функції format.

Функції moveTodoUp і moveTodoDown, з урахуванням розглянутих фрагментів, реалізуються досить просто і також використовують масив-накопичувач і тимчасовий файл:

void moveTodoUp (ref Terminal terminal, string [] arguments) {if (arguments.length & lt; 2) {terminal.color (Color.red, Color.black); terminal.writeln ( "\ nCommand \" head \ "has 2 arguments"); } Else {auto fileName = arguments.front; if (fileName.exists) {auto contents = (cast (string) std.file.read (fileName)) .splitLines; try {int index = to! size_t (arguments [1] .strip); string element = contents [index]; File temporaryFile; temporaryFile.open (fileName ~ `.temp`,` w`); (Element ~ contents.removeNth (index)) .each! (A = & gt; temporaryFile.writeln (a)); temporaryFile.close; remove (fileName); rename (fileName ~ `.temp`, fileName); terminal.color (Color.green, Color.black); terminal.writefln ( "\ nTodo with number% d was been moved to the top of list in file% s.", index, fileName); } Catch (Exception e) {terminal.color (Color.red, Color.black); terminal.writeln ( "\ nSecond argument must be a positive integer!"); }} Else {terminal.color (Color.red, Color.black); terminal.writeln ( "\ nFile" ~ fileName ~ "does not exists!"); }}} Void moveTodoDown (ref Terminal terminal, string [] arguments) {if (arguments.length & lt; 2) {terminal.color (Color.red, Color.black); terminal.writeln ( "\ nCommand \" tail \ "has 2 argument"); } Else {auto fileName = arguments.front; if (fileName.exists) {auto contents = (cast (string) std.file.read (fileName)) .splitLines; try {int index = to! size_t (arguments [1] .strip); string element = contents [index]; File temporaryFile; temporaryFile.open (fileName ~ `.temp`,` w`); (Contents.removeNth (index) ~ element) .each! (A = & gt; temporaryFile.writeln (a)); temporaryFile.close; remove (fileName); rename (fileName ~ `.temp`, fileName); terminal.color (Color.red, Color.black); terminal.writefln ( `Todo with number% d was been moved to the bottom of list in file% s.`, index, fileName); } Catch (Exception e) {terminal.color (Color.red, Color.black); terminal.writeln ( "\ nSecond argument must be a positive integer!"); }} Else {terminal.color (Color.red, Color.black); terminal.writeln ( "\ nFile" ~ fileName ~ "does not exists!"); }}}

Тепер можна розглянути одну з найцікавіших функцій програми todo - viewTodo. Працює вона з використанням вельми простого алгоритму: в разі успішного проходження всіх попередніх перевірок (кількість аргументів, існування файлу і т.д.) відбувається зчитування всього файлу в масив рядків, який потім нумерується, починаючи з нуля (за допомогою алгоритму enumerate), а потім виводиться в командний рядок за допомогою алгоритму each:

void viewTodo (ref Terminal terminal, string [] arguments) {if (arguments.empty) {terminal.color (Color.red, Color.black); terminal.writeln ( "\ nCommand \" view \ "has 1 argument"); } Else {auto fileName = arguments.front; if (fileName.exists) {terminal.color (Color.cyan, Color.black); writeln; auto contents = (cast (string) std.file.read (fileName)) .splitLines; contents .enumerate (0) .each! (a = & gt; writefln ( "% d -% s", a [0], a [1])); terminal.color (Color.green, Color.black); terminal.writefln ( "\ nYou have% d todo (s) now in file% s.", contents.length, fileName); } Else {terminal.color (Color.red, Color.black); terminal.writeln ( "\ nFile" ~ fileName ~ "does not exists!"); }}}

Природно, перед виведенням в командний рядок проводиться фарбування рядків в блакитний колір (при цьому, сам файл буде виведений в форматі »номер_запісі - запис»), а потім зеленим кольором буде виведена напис «You have% d todo (s) now in file% s. »(В файлі% s знаходиться% d заміток).

Копіюємо всі функції в один файл (не забуваємо про main) і компілюємо командою:

dmd todo.d asc1251.d terminal

І тестуємо:

Працює!

Функціональний стиль дозволяє набагато простіше і акуратніше висловити алгоритм програми, роблячи його досить витонченим і вишуканим. Застосування точкового нотації (ступінчаста обробка і використання UFCS в D) являє собою досить потужний засіб, яке в рівній мірі може як поліпшити читаність коду, так і погіршити її, при цьому чисте використання функціонального підходу в D може бути кілька непрактичним, що легко виправляється суміщенням його з традиційним імперативним підходом.

Звичайно, на одній конкретній ситуації не покажеш всіх нюансів функціонального програмування в D, тому я раджу читачам уважніше познайомиться зі стандартною бібліотекою D (зокрема, розділи std.algorithm, std.functional, std.range) і трохи попрактикуватися в якомусь чисто функціональній мові (Haskell, Scheme, Clojure і т.д).

PS: Автор статті від щирого серця дякує творців мови Haskell, Мірана Ліповача, Мохова Геннадія Володимировича за чудову бібліотеку QtE5 і Адама Руппі за чудову колекцію готових програмних рішень arsd.

A = & gt; (a! = "")?
Спочатку з arguments видаляється перший елемент (drop), після чого виділяються тільки непусті рядки (за допомогою алгоритму filter і анонімної функції a => (a! = "")?


Новости
  • Виртуальный хостинг

    Виртуальный хостинг. Возможности сервера распределяются в равной мере между всеми... 
    Читать полностью

  • Редизайн сайта

    Редизайн сайта – это полное либо частичное обновление дизайна существующего сайта.... 
    Читать полностью

  • Консалтинг, услуги контент-менеджера

    Сопровождение любых интернет ресурсов;- Знание HTML и CSS- Поиск и обновление контента;-... 
    Читать полностью

  • Трафик из соцсетей

    Сравнительно дешевый способ по сравнению с поисковым и контекстным видами раскрутки... 
    Читать полностью

  • Поисковая оптимизация

    Поисковая оптимизация (англ. search engine optimization, SEO) — поднятие позиций сайта в результатах... 
    Читать полностью