To work on my Haskell skills I decided to work on a little side project. I didn’t want something too complicated but I did want to try out some of the more fun and interesting tasks that Haskell does well.
As I work on the project I’ll go through the code, as a sort of tutorial on creating an actual application.
Haskell Project Links
I love productivity tools. I think mostly because I have difficultly being productive. I’ve used really complex applications that track burn down rates of tasks and give projections on project completion. But of course those tools require a lot of dedication or else they are useless. So for the past few years I’ve been a big fan of todo.txt. I use the .Net version on my work computer and the cli version on my Linux boxes.
Since I wanted a project to try some stuff out in Haskell, I thought why not build a command line implementation of todo.txt? It requires grammar parsing, and file I/O. Sprinkle in some Monads, Lenses and a few other topics and I’d be able to way over complicate this simple program.
To start off, I’m going to use stack to build this project. It seems to be the more de facto way to do Haskell development these days and it is backwards compatible with cabal in case I want to build this on a platform that isn’t supported by stack.
I run Gentoo no my laptop so I was able to install stack using its built in package manager, but stack has automated the process for those without such support.
$ curl -sSL https://get.haskellstack.org/ | sh
See the stack website for more details on installation, dependencies, etc.
So once stack is installed I created a new project. It seems like most of the project templates in stack are made for web development so I went with the default.
$ stack new todo Downloading template "new-template" to create project "todo" in todo/ ... The following parameters were needed by the template but not provided: author-email, author-name, category, cop yright, github-username, year You can provide them in /home/jeff/.stack/config.yaml, like this: templates: params: author-email: value author-name: value category: value copyright: value github-username: value year: value Or you can pass each one as parameters like this: stack new todo new-template -p "author-email:value" -p "author-name:value" -p "category:value" -p "copyright:va lue" -p "github-username:value" -p "year:value" Using cabal packages: - todo/todo.cabal Selecting the best among 4 snapshots... * Selected lts-6.9 Initialising configuration using resolver: lts-6.9 Writing configuration to file: todo/stack.yaml All done. $
The next step is to edit the cabal file.
name: todo version: 0.0.1.0 synopsis: A haskell implementation of todo.txt description: Please see README.md homepage: https://github.com/jecxjo/todo#readme license: BSD3 license-file: LICENSE author: Jeff Parent maintainer: firstname.lastname@example.org copyright: 2016 Jeff Parent category: Productivity build-type: Simple -- extra-source-files: cabal-version: >=1.10 library hs-source-dirs: src exposed-modules: Lib build-depends: base >= 4.7 && < 5 default-language: Haskell2010 executable todo-exe hs-source-dirs: app main-is: Main.hs ghc-options: -threaded -rtsopts -with-rtsopts=-N build-depends: base , todo default-language: Haskell2010 test-suite todo-test type: exitcode-stdio-1.0 hs-source-dirs: test main-is: Spec.hs build-depends: base , todo ghc-options: -threaded -rtsopts -with-rtsopts=-N default-language: Haskell2010 source-repository head type: git location: https://github.com/jecxjo/todo
At this point we can build the project and see that it runs the default Hello World code.
$ stack build todo-0.0.1.0: configure Configuring todo-0.0.1.0... todo-0.0.1.0: build Preprocessing library todo-0.0.1.0... [1 of 1] Compiling Lib ( src/Lib.hs, .stack-work/dist/x86_64-linux/Cabal-18.104.22.168/build/Lib.o ) In-place registering todo-0.0.1.0... Preprocessing executable 'todo-exe' for todo-0.0.1.0... [1 of 1] Compiling Main ( app/Main.hs, .stack-work/dist/x86_64-linux/Cabal-22.214.171.124/build/todo-exe/ todo-exe-tmp/Main.o ) Linking .stack-work/dist/x86_64-linux/Cabal-126.96.36.199/build/todo-exe/todo-exe ... todo-0.0.1.0: copy/register Installing library in /home/jeff/devel/blogexample/todo/.stack-work/install/x86_64-linux/lts-6.9/7.10.3/lib/x86_64-linux-ghc-7.10.3/t odo-0.0.1.0-0JhiS6FdmLKA3OMxdXl6BR Installing executable(s) in /home/jeff/devel/blogexample/todo/.stack-work/install/x86_64-linux/lts-6.9/7.10.3/bin Registering todo-0.0.1.0... $ stack exec todo-exe someFunc $
Quick Overview of todo.txt
todo.txt has a fairly simple set of rules. Using a text file, each line contains either an incomplete or completed task. Each task can contain meta data such as priority, a start and end date, flags denoting a project and contextual information. It goes along with the whole Getting Things Done method of running your life. And being a simple text file, its easily expandable to add in third-party features without making backwards compatibility difficult.
Priority is denoted by starting a task line with a parenthesized letter. (A) is high and (Z) is low and omitting it makes it have no priority.
(A) A high priority task (B) A little lower priority task A no-priority task
After the optional priority comes an optional start date. The format is
(A) 2016-07-30 A high priority task with a start date 2016-07-30 A no priority task with a start date A bland task
Project and Context
A project is defined as a word starting with a ’+’. Context is given by starting a word with ’@’. There can be multiples of both types throughout the rest of the task description.
(A) 2016-07-30 Call Mom +LifeStuff @birthday @DoNotForget Pick up milk +GroceryList
When a task is completed, the line is appended with an ‘x’ and then a completion date.
x 2016-07-30 (A) 2016-07-30 Call Mom +LifeStuff @birthday @DoNotForget Pick up milk +GroceryList
There is a few more rules that need to be followed, but for the moment thats all we need to focus on in this post.
So lets start by creating the data structures that will store our tasks. Creating a file
src/Tasks.hs, we know that there are two types of Tasks: Incomplete and Completed
module Tasks where data Tasks = Incomplete String | Completed String
Since we will want to be doing things like sorting, and filtering based on all the meta data in a task it makes sense that we would want to have each assess to said meta data. Let define types for each of the meta data tokens.
type Priority = Char -- (A) type Project = String -- +ProjectName type Context = String -- @Context data Date = Date Int Int Int deriving Show
From the definition of an incomplete task, we can have an optional priority, an optional start date and multiple context and projects. With those rules in mind our
Tasks data type changes to
data Tasks = Incomplete (Maybe Priority) (Maybe Date) [Project] [Context] String | Completed String
String at the end can store the actual task information the user enters. A completed task is the same as an incomplete one except it has a required completion date (and an x but we don’t need that in our data structure). The simplest way to do that is make
Completed contain an Incomplete.
data Tasks = Incomplete (Maybe Priority) (Maybe Date) [Project] [Context] String | Completed Date Tasks deriving Show
Before we can build and test we need to add in our new module to our cabal file:
library hs-source-dirs: src exposed-modules: Lib , Tasks build-depends: base >= 4.7 && < 5 default-language: Haskell2010
All new modules need to be added here when you compile. To build run
$ stack build todo-0.0.1.0: configure Configuring todo-0.0.1.0... todo-0.0.1.0: build Preprocessing library todo-0.0.1.0... [2 of 2] Compiling Tasks ( src/Tasks.hs, .stack-work/dist/x86_64-linux/Cabal-188.8.131.52/build/Tasks.o ) In-place registering todo-0.0.1.0... Preprocessing executable 'todo-exe' for todo-0.0.1.0... Linking .stack-work/dist/x86_64-linux/Cabal-184.108.40.206/build/todo-exe/todo-exe ... todo-0.0.1.0: copy/register Installing library in /home/jeff/devel/blogexample/todo/.stack-work/install/x86_64-linux/lts-6.9/7.10.3/lib/x86_64-linux-ghc-7.10.3/t odo-0.0.1.0-0JhiS6FdmLKA3OMxdXl6BR Installing executable(s) in /home/jeff/devel/blogexample/todo/.stack-work/install/x86_64-linux/lts-6.9/7.10.3/bin Registering todo-0.0.1.0... $
We can then load the GHCI REPL by running
$ stack ghci todo-0.0.1.0: build Preprocessing library todo-0.0.1.0... Configuring GHCi with the following packages: todo GHCi, version 7.10.3: http://www.haskell.org/ghc/ :? for help [1 of 3] Compiling Tasks ( /home/jeff/devel/blogexample/todo/src/Tasks.hs, interpreted ) [2 of 3] Compiling Lib ( /home/jeff/devel/blogexample/todo/src/Lib.hs, interpreted ) [3 of 3] Compiling Main ( /home/jeff/devel/blogexample/todo/app/Main.hs, interpreted ) Ok, modules loaded: Lib, Tasks, Main. *Main Lib Tasks> let t1 = Incomplete (Just 'A') (Just $ Date 2016 7 30) ["Todo"] ["Blog"] "Work on +Todo @Blog Post" *Main Lib Tasks> t1 Incomplete (Just 'A') (Just (Date 2016 7 30)) ["Todo"] ["Blog"] "Work on +Todo @Blog Post" *Main Lib Tasks> let t2 = Completed (Date 2016 7 30) t1 *Main Lib Tasks> t2 Completed (Date 2016 7 30) (Incomplete (Just 'A') (Just (Date 2016 7 30)) ["Todo"] ["Blog"] "Work on +Todo @Blog Post") *Main Lib Tasks> :q Leaving GHCi.
We can create an
Completed task and print them out (using the derived Show class).
Next post we’ll make our data structures print better, sort, and filter.