Haskell can be a very easy language to develop in, requiring few tools besides a compiler. I’m a crazy person and prefer to use a basic text editor to edit and a command line shell to compile. I find IDEs generally to not be worth the performance and stability overhead, especially with a powerful compiler like GHC. But, sometimes it’s nice to be assured that your code is correctly compiling (and running tests) in the background while you develop.

To achieve this, I’m going to show you how to build your own local, continuous build system for Haskell. It’s easier than you think.

This post applies to Linux users only, due to build dependencies. Hackage does not yet have a corresponding package for Windows.

This post also assumes a basic but passable familiarity with developing in Haskell. If you don’t know how to use cabal yet, this is probably a bit advanced for you.

Required Packages

First, you’ll want to get the hinotify package from Hackage. From a command line:

cabal update && cabal install hinotify

This is a lightweight package that wraps around the Linux Kernel’s inotify feature. It provides many capabilities for listening to events on a file handle, but we’re just going to follow the basics: Listening for writes that add, change, or remove files.

Now that that’s done, whip up a new file and start writing your Haskell:

Setting up our Imports

import System.Directory import System.Environment (getArgs) import System.INotify import System.IO

Your mileage may vary, but the key piece is to import System.INotify. The rest is mainly for syntactic sugar for our example.

Managing the Watcher

Initializing an inotifier is pretty easy:

n <- initINotify

To add a watch to an inotifier, you’ll need to specify a list of EventVariety‘s, the directory to watch, and a function to catch events:

wd <- addWatch n -- our inotifier [ Modify, CloseWrite, Create, Delete, MoveIn, MoveOut ] -- the EventVariety types to watch for dir -- the directory to monitor eventHandler -- the event handling function

We’ll need to remove our file watcher when we’re done with it:

removeWatch wd

And don’t forget to close your inotifier before exiting:

killINotify n

In production code, you’ll want to bracket both the inotifier and the watcher, to make sure all of our resources are closed properly.

Putting this all together in main (with some added boilerplate for basic I/O), we get:

main = do args <- getArgs let dir = head args putStrLn $ "Watching directory: " ++ dir n <- initINotify putStrLn "Press <Enter> to exit" print n wd <- addWatch n [ Modify, CloseWrite, Create, Delete, MoveIn, MoveOut ] dir eventHandler print wd getLine removeWatch wd killINotify n

Handling File Events

For the event handler, we’ve set up a pretty simple pattern matched function. We only match against those Event‘s we’re interested in, discarding the rest. For the matching events, we’ll pass the event and the file path down to a filter function (more on that in a bit). We could just pass the file path itself for our simple little continuous builder, but we’ll use the event to monitor what kinds of messages we’re handling.

eventHandler :: Event -> IO () eventHandler x@(Modified _ (Just fp)) = handleFilteredFile x fp eventHandler x@(MovedIn _ fp _) = handleFilteredFile x fp eventHandler x@(MovedOut _ fp _) = handleFilteredFile x fp eventHandler x@(Created _ fp) = handleFilteredFile x fp eventHandler x@(Deleted _ fp) = handleFilteredFile x fp eventHandler _ = return ()

Filtering Events by File Type

You can filter file types in different ways. We’re going to keep this pretty basic: If the file ends in a .hs extension, let’s do some work on it, otherwise, let’s ignore it.

-- If the file is a .hs file, output the observed file event and do some work on -- the file. Otherwise, just ignore it. handleFilteredFile evt fp = do if filterHS fp then print evt >> doWork fp else return () -- Simply check that the file extension is ".hs" filterHS fp = (== "hs") $ reverse $ takeWhile (/= '.') $ reverse fp

Do Some Work

Unless you prefer sipping mai tais on a beach, we’ll now need to do some work on our watched file. But for this post (and because we like sipping mai tais!) we’re just going to do nothing:

doWork :: FilePath -> IO () doWork fp = return ()

Wrapping up

That’s it! If you run this in ghci or as an executable, you’ll see events start firing as you move .hs files around or write changes to .hs files in your monitored directory. Just press <Enter> to exit the program.

In the next part, we’ll take a look at doing something useful with our file path input.

As always, the full cabal project is available on https://github.com/stormont/continuous-hs. To get the version used in this post, use the tag part-1.

Here’s the full code we’ve developed:

import System.Directory import System.Environment (getArgs) import System.INotify import System.IO main = do args <- getArgs let dir = head args putStrLn $ "Watching directory: " ++ dir n <- initINotify putStrLn "Press <Enter> to exit" print n wd <- addWatch n [ Modify, CloseWrite, Create, Delete, MoveIn, MoveOut ] dir eventHandler print wd getLine removeWatch wd killINotify n eventHandler :: Event -> IO () eventHandler x@(Modified _ (Just fp)) = handleFilteredFile x fp eventHandler x@(MovedIn _ fp _) = handleFilteredFile x fp eventHandler x@(MovedOut _ fp _) = handleFilteredFile x fp eventHandler x@(Created _ fp) = handleFilteredFile x fp eventHandler x@(Deleted _ fp) = handleFilteredFile x fp eventHandler _ = return () handleFilteredFile evt fp = do if filterHS fp then print evt >> doWork fp else return () filterHS fp = (== "hs") $ reverse $ takeWhile (/= '.') $ reverse fp doWork :: FilePath -> IO () doWork fp = return ()