How Many Gardening Metaphors Does It Take To Boostrap A New Haskell Project?

In our last post we introduced togglemon, a small utility to manage the toggling of monitors under Linux1. Having the idea is, in many cases, the easy part. Almost as soon as I have an idea, a whole host of little problems start arising, all seeking to nip my idea in the bud. Ideas can wither on the vine for a number of reasons. Waning motivation, lack of skills, and other factors may starve an idea of the valuable resources needed to bear fruit. Knowing this, I try to build habits and use tools that help me nurture ideas. I want to focus only on the aspects of a project that encourage the growth of an idea from seed to flower.

So today let's set aside togglemon and take a peek into the process of preparing the soil for whatever ideas may be sprouting. We'll go from a meager plot of command line real estate to a full fledged garden that is equipped to cultivate all sorts of ideas!

To get the most of out today's exploration I'll assume you're at least aware of the following tools. It's okay if you're not comfortable with some of them, we'll explore them in more detail as we go along:

  • A terminal emulator, running on Linux (I currently use kitty)2
  • A superficial understanding of Haskell
  • A superficial knowledge about one of its build tools, Stack

Without further ado, let's get gardening to work!

Preparing the Garden with Stack

In projects of any size I like having a template to start from. This allows me to quickly jot my thoughts down into some runnable code before the inspiration is lost. This functionality is typically provided by a build tool in the ecosystem of your language, and since we'll be using Haskell, we'll use Stack to provision our new project. The community appears split on Stack and Cabal, and the differences appear mostly academic to me. I've stuck with Stack because its workflow does what I want and makes it pretty painless to build Docker containers (i.e. when writing Haskell-based lambdas). Let's bust out Stack in our terminal and start assembling the raised beds for our ideas:

$ stack new togglemon simple-hpack

What we've done is tell Stack that we want a new project called togglemon, and we'd like it to use the simple-hpack template as the base for our project. No, dear reader, I didn't pull simple-hpack out of thin air, it comes included with stack in the stack-templates project, where many more project templates can be found.

Hopefully while you've been reading this the bootstrap command has finished running. If it hasn't, follow along anyway while shaking your fist angrily at your straw man of choice (I'm partial to blaming the lizard people). You'll see something like this in your current directory:

$ tree .
.
`-- togglemon
    |-- LICENSE
    |-- README.md
    |-- Setup.hs
    |-- package.yaml
    |-- src
    |   `-- Main.hs
    |-- stack.yaml
    `-- togglemon.cabal

2 directories, 7 files

This gives us just enough structure to plant a wide range of ideas without forcing us to decide if we need a trellis, poles, or other specialized tools. When our idea matures, we can begin to layer on additional project structure according to it's needs. Maybe we'll need multiple modules. Maybe we need to split out library code from application code. The point is to only add these patterns when we need them so that we don't back ourselves into a project corner that kills our motivation to continue cultivating.

Fertilizing the Soil with Common Settings

Before I start planting in earnest, though, I'll typically modify the package.yaml file to include some configuration that makes writing Haskell more streamlined3. package.yaml is the hpack variant of our project's configuration and is a wrapper around the typical {project-name}.cabal file you may see in Cabal-only projects. It controls how the project is built, what dependencies we pull into our project, and how our unit and performance tests are dealt with. It is complementary to Stack in the sense that hpack is responsible for how our project is built, and Stack is responsible for the context in which it is built.

To that end, Alexis King wrote a fantastic article on how they write Haskell that is the source of much of my current workflow. Since my Haskell code tends to be a bit less adventurous, I'll typically only enable a subset of what Alexis uses in their article:

# in package.yaml
# ...

default-extensions:
  - BangPatterns
  - DefaultSignatures
  - DeriveFoldable
  - DeriveFunctor
  - DeriveGeneric
  - DeriveLift
  - DeriveTraversable
  - EmptyCase
  - FlexibleContexts
  - FlexibleInstances
  - FunctionalDependencies
  - GeneralizedNewtypeDeriving
  - InstanceSigs
  - LambdaCase
  - MultiParamTypeClasses
  - MultiWayIf
  - OverloadedStrings
  - PatternSynonyms
  - ScopedTypeVariables
  - TemplateHaskell

# ...

It's sufficient to include this in your own config, but if at any point you're curious about what any of these extensions do, Alexis' descriptions are as good a starting point as any. There's also the official GHC language extension documentation that explains tool-agnostic strategies for enabling language extensions in your project and complete documentation for each extension.

In addition to language extensions I'll also enable a number of GHC compiler flags that will either fail or warn me of typically poor user behavior. I'm fond of these kinds of features because, like my fellow gardeners, I want to give my ideas every advantage to grow. Mitigating common sources of programmer error make it more likely that our ideas will take their correct form, and therefore more likely to be useful.

# also in package.yaml
#...

ghc-options:
  - -Wall
  - -Wcompat
  - -Wincomplete-record-updates
  - -Wincomplete-uni-patterns
  - -Wredundant-constraints
  - -fno-warn-partial-type-signatures
  - -fno-warn-name-shadowing
  - -fwarn-tabs
  - -fwarn-unused-imports
  - -fwarn-missing-signatures
  - -fwarn-incomplete-patterns

#...

I'll again refer you to Alexis' article for a more robust explanation of the above options, but suffice it to say this reduces a good chunk of typical mistakes when iterating on an idea in Haskell.

Typical Planting Development Workflow

Now that we've got our configuration fertilizer distributed it's time to start planting some of those heady ideas. When exploring a particular idea I like to dump myself in a Read Eval Print Loop (REPL), which allows me to toy with types interactively and query GHC for information about library functions. Getting into a REPL is pretty straightforward:

$ stack repl
Using main module: 1. Package `togglemon' component exe:togglemon with main-is file: /tmp/tmp.S5zXdymUxR/togglemon/src/Main.hs
Building all executables for `togglemon' once. After a successful build of all of them, only specified executables will be rebuilt.
togglemon-0.1.0.0: configure (exe)
Configuring togglemon-0.1.0.0...
togglemon-0.1.0.0: initial-build-steps (exe)
Configuring GHCi with the following packages: togglemon
GHCi, version 8.6.5: http://www.haskell.org/ghc/  :? for help
[1 of 1] Compiling Main             ( /tmp/tmp.S5zXdymUxR/togglemon/src/Main.hs, interpreted )
Ok, one module loaded.
Loaded GHCi configuration from /tmp/haskell-stack-ghci/1a5c6030/ghci-script
*Main> 

You'll notice that Stack has helpfully linked our package (and eventually its dependencies) into the REPL, so whenever we feel the urge to explore some area of our codebase we can do so with ease. Once inside, it's useful to know a few key commands:

  • :i or :info to get information on a particular type and its instances or an expression.
    • Try :i Maybe to get a sense for what this looks like.
  • :t or :type to get the infered type of a particular expression.
    • Try :t (+) to see the type of the addition operator, and :t (+) 1 to see how that type changes when you apply arguments to it.
  • :r or :reload will reload everything in the current REPL session.
    • Try :r after you've modified something in src/Main.hs and see how the REPL reloads your code. If you add something that doesn't compile, the REPL will tell you what failed, but continue accepting input.
  • :set will change various REPL options for you (with auto completion).
    • Try :set -XOverloadedStrings to enable the OverloadedStrings extension
    • Also try :set prompt "> ", which resets your prompt to the string > . Useful when your project grows and you're loading potentially dozens of modules in the REPL and want a cleaner prompt.

You can even write multi-line statements in the REPL with the help of the :{ and :} commands. I find myself using this feature when I want to define something in the REPL and I want to be clear about what my types are going to be, but any expression that's clunky to write on a single line will benefit from this style. Here's a contrived example to show you how this can work in practice:

*Main> :{
*Main| myFn :: Num a => a -> a -> a
*Main| myFn x y = (x - y) + (y - x)
*Main| :}
*Main> myFn 1 2
0

While the REPL is useful for piloting new ideas, it's less ideal for modifying existing code or validating against a test suite. For this purpose, it's more useful to have an editor open in one window, and a shell in another that is automatically rebuilding the project. Stack is well-suited for this purpose, and provides a number of build flags to get the desired output that we want.

Here's a screenshot of my setup on a typical day:

On the left two thirds of the screen is my editor of choice (vim), while Stack resides on the right-hand side and is rebuilding my test suite.

As you may be able to infer from the editor screenshot above, the feedback loop on this cycle takes under a second, which I find acceptable. The biggest factor that will slow down your feedback loop will be tightly coupled modules that necessitate a re-compile of most of the project.

This setup can be achieved by running the following command in your terminal:

# -- Invoke stack
# |     -- Tell it to build the project
# |     |     -- The sub-project to build. *Most* projects can omit this
# |     |     |         -- Only re-compile the modules that have changed since last invocation
# |     |     |         |      -- Run our test suite as configured in project.yaml
# |     |     |         |      |      -- Watch the project for any changes
# |     |     |         |      |      |
# v     v     v         v      v      v
$ stack build togglemon --fast --test --file-watch

Stack provides a number of flags for --build that can perform tasks like test coverage checks, documentation generation, and other useful bits and bobs. While the command above is my bread and butter when developing Haskell projects, I do ocassionally change the flags if I'm focusing on a particular task. For example, if I'm reaching a point in a project's lifecycle were a documentation sweep needs to be done, I'll exchange the --test for --haddock flags4 and keep a browser tab open to make sure I haven't mucked up the Haddock syntax.

Finally, if I'm working on a project that produces an executable binary of some sort (whether that's a CLI or an AWS Lambda function), I'll add the --copy-bins flag so that it ends up in my global Stack bin folder (typically ~/.local/bin).

Logging Seasonal Growth with GitHub Issues

Sometimes when I start a new project I have a pretty limited goal in mind. Maybe it's to test out a new library, or to demonstrate a pattern to a colleague. Other times, I'm working on something that I'll get mileage out of, like togglemon or aws-ddns. In these situations it's nice to keep track of what I intend to build and the research that goes along with it. I used to scribble notes down on paper but that strategy is really only useful for me in a professional setting, where the set of projects is fixed and in a related domain.

The goal here is to keep a running list of features that I'd eventually like to implement along with enough information about past progress. It's essentially a development log, organized in Kanban format so that I roughly know what the status of everything is. The Kanban format is helpful for me because it gives me a few pieces of information quickly:

  • What feature did I last complete?
  • What is currently in progress?
  • What's on the horizon?

For those familiar with various flavors of "agile" that should sound pretty standard. Here's what one of these boards look like:

It doesn't have to be terribly complicated, three swimlanes are enough to get a sense of where things are at. Card titles should be descriptive enough to understand what effect will have taken place when the card is done, and card labels give you additional context. GitHub even provides some light automation, so, for example, if you close an issue it'll automatically be sent to the Done column.

Like many gardeners that keep track fo seasonal variation, a Kanban board is an invaluable tool to track progress over time towards a particular goal.

Putting It All Together

We've covered a few tools and strategies for nurturing ideas in Haskell. To summarize:

  • Use Stack to bootstrap your project structure
  • Sprinkle in a few common language extensions
  • Mix in some common compiler warnings
  • Experiment at the REPL with stack repl
  • Iterate quickly using Stack build flags
  • Manage project progress with GitHub Issues

With the above workflow I find I can bootstrap and be productive with a new project in less than 5 minutes. For my workflow, that's enough time to start getting in the implementation mindset and make the most of whatever time I have left (usually another 20 minutes or so before another interruption comes in). Couple that with a few minutes to document where I'd like to take a project next and I can confidently switch between projects while minimizing the impact of context switching.

That said, much of the advice here can be generalized further to your language of choice. The basic principles are:

  • Find a bootstrap/template tool for your language that can quickly setup a simple project
  • Define idiomatic language features, packaging options, etc, for your language
  • Use a system of record to track project progress

Next time, we'll apply these strategies to toggelmon and begin implementing a basic solution to the problem of toggling monitors under Linux.

Footnotes

  1. You see what I did there? Eh??? EHHHH?

  2. If you want to follow along and you aren't on Linux, have you really been following this post series at all? ಠ_ಠ

  3. Let's not forget, friends, that Haskell is borne out of the crucible of academia, and as such is the fertile playground of many creative minds over its nearly 30 year (!) history. To support all that creativity GHC, Haskell's defacto compiler, supports a system of language extensions that have allowed the language to evolve as new programming patterns emerge while maintaining a comparatively small and stable core language.

  4. Haddock, while a tasty fish, refers to Haskell's documentation system in this instance