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.
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
.
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.
I hope it was easy and most of all I hope you've had fun following along.