Stack Builders logo
Arrow icon Insights

Having fun with Emacs in Haskell

"Good clean fun with haskell-emacs"

When embarking on a journey to learn Haskell the road can often look daunting and intimidating. Along they way a programmer will inevitably approach troublesome avenues and endeavor to summit mind-wrenching peaks. Package management issues, abstract mathematical concepts, and philosophical debates abound but still, with Haskell, there is much fun to be had around every corner.

In this post we are going to relax and have some good, clean, easy fun by writing an Emacs extension using a nice plugin dubbed haskell-emacs. The plugin can expose our Haskell functions from Emacs Lisp via macros and automatically marshal selected types across the language boundary.

The goal of our extension is to allow the selection of build targets found in a .cabal file. The Haskell community has made available a plethora of text and grammar manipulation tools which are excellent for writing editor extensions. Those extensions which pertain to Haskell itself are of course particularly well supported.

In the spirit of rapid-development, below we have a rapid-prototype of what we intend accomplish. After all, a prototype is worth a thousand explanations. Be sure to click in the editor frame before trying the prototype functionality.

<iframe src="/static/news/haskell-emacs-prototypes/ymacs/test/index.html" sandbox="allow-scripts allow-same-origin" scrolling="no" frameborder=0 style="width : 688px; height : 300px; border: solid 1px #CCCCCC; border-radius: 5px 5px 5px 5px;"> </iframe>

Before starting it is important to note that the instructions you are about to encounter were tested against GHC 7.8.3; when following along please use that version. If you find this to be a worrisome matter, it may be time to invest in a version management solution.

We begin by setting up the haskell-emacs plugin. Assuming you have a recent version of Emacs with the package manager configured to draw packages from the melpa archive, then the first step is simple; run the following command from within Emacs:

M-x package-install RET haskell-emacs

Wonderful! Now let's make a Cabal sandbox to help us create a package database which will contain the dependencies that both haskell-emacs itself and our extension will rely upon. Pick a nice spot in the directory where you tend put things such as this and then enter the following commands:

mkdir haskell-emacs-exts
cd haskell-emacs-exts
cabal sandbox init
cabal install attoparsec-0.12.1.6
cabal install atto-lisp-0.2.2

Cool. If you should get more ambitious with your extensions, then you may want to use a .cabal file to express dependencies but the above should be good enough to get us started.

The next step is to update your .emacs or init.el file. Open your .emacs or init.el file and write (require 'haskell-emacs) on a line somewhere after your (package-initialize) expression (if you don't have a (package-initialize) expression, then add it). You must now either restart Emacs or evaluate the buffer:

M-x eval-buffer

Now augment the GHC command that haskell-emacs will use to compile our extension by telling it to look for dependencies in our new package database.

M-x customize-variable RET haskell-emacs-ghc-flags

Simply add the appropriate flag, -package-db, in the space provided by the textual GUI. Point this flag to the location of the package database you've just created which is found in the Cabal sandbox.

Package Database

Again open your Emacs configuration file and add (haskell-emacs-init) somewhere after your (custom-set-variables...) expression. Restart emacs or evaluate the buffer.

By default the haskell-emacs package creates a directory for us called haskell-fun in our .emacs.d directory. This is where we are going to put our extension file; we will call it Cabal.hs. So create the file .emacs.d/haskell-fun/Cabal.hs and open it for editing.

Everything is set up so let's quickly hash out what we want to do. We want a Haskell function that parses the contents of a .cabal file and collects the names of the build targets found within. The function signature can look as follows:

module Cabal where

type CabalFile       = String  -- The contents of a .cabal file
type BuildTargetName = String  -- The parsed names

buildTargetNames :: CabalFile -> [BuildTargetName]

Obviously the first thing we want to do is parse the contents of the .cabal file that we are being passed. Given that we are using Haskell there is nothing stopping us from using the Cabal package itself to parse the content.

module Cabal where

import Distribution.PackageDescription.Parse  (parsePackageDescription
                                              ,ParseResult(ParseFailed
                                                          ,ParseOk))

type CabalFile       = String  -- The contents of a .cabal file
type BuildTargetName = String  -- The parsed names
type ParseError      = String  -- Parsing error messages

-- | Returns a list of build targets in a cabal file.
buildTargetNames :: CabalFile -> Either ParseError [BuildTargetName]
buildTargetNames cabalFile =
  case parsePackageDescription cabalFile of
    ParseFailed parseError        -> Left $ show parseError
    ParseOk _ packageDescription  -> Right $ buildTargetNames' packageDescription

Parsing can fail, so we've modified the return type of buildTargetNames to reflect the possibility. Here, haskell-emacs will marshal the Either into the type on the left, a string, in case of failure and into the type on the Right, a list, in the case of success. Thus we have a criteria to check against.

Upon successful completion parsePackageDescription returns what the Cabal package calls a GenericPackageDescription; this type of thing holds the build targets which we are now free to extract.

{-# LANGUAGE RecordWildCards #-}

module Cabal (buildTargetNames) where

import Control.Applicative                    ((<$>))
import Data.Monoid                            ((<>))
import Distribution.PackageDescription        (GenericPackageDescription(..))
import Distribution.PackageDescription.Parse  (parsePackageDescription
                                              ,ParseResult(ParseFailed
                                                          ,ParseOk))

type CabalFile       = String  -- The contents of a .cabal file
type BuildTargetName = String  -- The parsed names
type ParseError      = String  -- Parsing error messages

-- | Returns a list of the build targets in a cabal file.
buildTargetNames :: CabalFile -> Either ParseError [BuildTargetName]
buildTargetNames cabalFile =
  case parsePackageDescription cabalFile of
    ParseFailed parseError        -> Left $ show parseError
    ParseOk _ packageDescription  -> Right $ buildTargetNames' packageDescription


buildTargetNames' :: GenericPackageDescription -> [BuildTargetName]
buildTargetNames' GenericPackageDescription{..} =  (fst <$> condExecutables)
                                                <> (fst <$> condTestSuites)
                                                <> (fst <$> condBenchmarks)
                                                <> maybe [] (const ["default"])
                                                            condLibrary
{-# inline buildTargetNames' #-}

That was easy! Now a call to haskell-emacs-init will give the Emacs side of things access to our newly-created functionality (or errors if we've made mistakes). Also, you'll notice how the documentation for our exported function is made available through Emacs by using the usual C-h f or M-x describe-function.

Haskell Emacs Docs

Considering that we are programming for Emacs let's let Emacs Lisp handle all of our I/O operations, the first of which will simply call our macro. Let's place these functions directly in our .init.el file for the time being. We need both a function to wrap the call to the newly-exposed macro and another function to read the contents of a cabal file to a string. To start, put the following somewhere near the top of your Emacs initialization file (.emacs/.init.el) if it's not already there:

(require 'cl)

Now for our two functions:

(defun get-target-names-from-cabal-file (filename)
  "Return the build targets from a cabal file."
  (let ((eitherErrorTarget (Cabal.buildTargetNames
                             (file-contents filename))))
    (if (listp eitherErrorTarget)
      eitherErrorTarget
      (error eitherErrorTarget))))


(defun file-contents (filename)
  (with-temp-buffer
    (insert-file-contents-literally filename)
    (buffer-substring-no-properties (point-min) (point-max))))

Great, with a valid path to a .cabal file we can now list the build targets found within! It would, however, be much nicer if our little extension could automatically find the .cabal file associated with a given Haskell source file, if any. To do this let's assume that the first .cabal file encountered while traveling up a directory tree from a Haskell file's directory is the one responsible for that file. This of course is not a reliable assumption but it's a good place to start.

(defun find-cabal-file (dir)
  "Recurse up a directory in search of a .cabal file."
  (if (string= "/" dir)
      (error "not in a cabal project")
    (let ((cabal-files (directory-files dir t ".\\.cabal$")))
      (if (not (null cabal-files))
          (car cabal-files)
        (find-cabal-file (expand-file-name "../" dir))))))

Now that we have all our pieces let's make a top-level interactive function that finds our .cabal file, gets our build-targets, and then feeds them to ido for fancy search/selection.

(defun select-cabal-build-target ()
  (interactive)
  (let* ((cabal-filename (find-cabal-file default-directory))
         (build-targets (get-target-names-from-cabal-file cabal-filename)))
    (ido-completing-read "select cabal build target: " build-targets)))

Finally, we've completed our goal of automatically grabbing a selection of build targets from a .cabal file.

Select Target Capture

I hope it was easy and most of all I hope you've had fun following along.

Published on: Jun. 24, 2015
Last updated: Dec. 20, 2024

Written by:

Software Developer
Eric C. Jones

Subscribe to our blog

Join our community and get the latest articles, tips, and insights delivered straight to your inbox. Don’t miss it – subscribe now and be part of the conversation!
We care about your data. Check out our Privacy Policy.