Haskell Project: Stack and Data Types
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:
Intro
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][1]. 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.
Stack
To start off, I'm going to use [stack][2] 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][2] 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: jeff@commentedcode.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-1.22.8.0/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-1.22.8.0/build/todo-exe/ todo-exe-tmp/Main.o ) Linking .stack-work/dist/x86_64-linux/Cabal-1.22.8.0/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][3]. 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
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
Start Date
After the optional priority comes an optional start date. The format is `YYYY-MM-DD`.
(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
Completed Tasks
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.
Data Structure
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
The `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`.
$ 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-1.22.8.0/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-1.22.8.0/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`.
$ 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 `Incomplete` and `Completed` task and print them out (using the derived Show class).
[Next post][5] we'll make our data structures print better, sort, and filter.
$ date: 2016-07-30 17:46 $
$ tags: haskell, tutorial, stack $
-- CC-BY-4.0 jecxjo 2016-07-30