Haskell Project: Stack and Data Types

:: haskell, tutorial, stack

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. 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 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:          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. 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 we’ll make our data structures print better, sort, and filter.