nix-eval for Haskell
The nix-eval
package scratches a particular itch for me. It allows Haskell code
to be evaluated at run-time, in a sub-process, using Nix to get any dependencies it
needs.
NOTE: This page is active code, so check out the “view source” link at the bottom of the page if you want to follow along with the examples!
As tradition dictates, we’ll start with "hello world"
,
which is trivial to do using Haskell’s built-in String
type:
= putStr "hello world" main
TODO: Disabled for space reasons
However, if we try to write "hello world"
using the
popular Text
library, it breaks:
= putStr (Data.Text.unpack (Data.Text.pack "hello world")) main
WARNING: Skipping nix_eval execution for space reasons
TODO: Disabled for space reasons
In order to use functions like Data.Text.unpack
, we
first need to import the Data.Text
module:
import Data.Text
= putStr (Data.Text.unpack (Data.Text.pack "hello world")) main
WARNING: Skipping nix_eval execution for space reasons
TODO: Disabled for space reasons
Another error! In order to import modules like
Data.Text
, we first need to register the text
package. This can’t actually be done from within Haskell; we need to
perform this step before invoking the Haskell compiler/interpreter.
Most of the time that’s fine, and we have tools like Cabal and Nix to help us:
import Data.Text
= putStr (Data.Text.unpack (Data.Text.pack "hello world")) main
TODO: Disabled for space reasons
However, this falls apart when we want to evaluate Haskell code at
run-time, ie. using an eval
function.
Run-time code evaluation is well-known in scripting languages, and
although it’s a little tricky, it can be done in Haskell too. However,
most implementations of eval
rely on the packages and
modules which are available to the “host” program, rather than what is
needed by the given expression.
That’s where nix-eval
comes in. It provides a simple
Expr
data type which contains a String
of code
to evaluate, along with a list of packages and modules it needs. During
evaluation, these dependencies are fetched using Nix, and the code is
sent to a new instance of GHC, which has those packages available.
For example, our simple "hello world"
would become:
import Language.Eval
= do result <- eval (asString "hello world")
main case result of
Nothing -> error "Didn't work :("
Just x -> putStr x
TODO: Disabled for space reasons
As you can see, it’s a bit more verbose. In particular, the output
type of eval
is IO (Maybe String)
, since
evaluation is not a pure function (IO
) and it may fail
(Maybe
). The String
is the standard output of
the GHC process which Nix invokes, which we’re free to parse in any way
we like.
Next we can try the text
example, but this time we won’t
import or register anything besides nix-eval
, like
above:
import Language.Eval
= withPkgs ["text"] . qualified "Data.Text"
dt
= do result <- eval (dt "unpack" $$ (dt "pack" $$ asString "hello world"))
main case result of
Nothing -> error "Didn't work :("
Just x -> putStr x
TODO: Disabled for space reasons
nix-eval
has a few simple combinators, including
$$
which applies one Expr
to another (like
Haskell’s $
); withPkgs
and
withMods
which append to an Expr
’s context;
and qualified
which appends to the context and
prefixes the expression.
I wrote this to help simplify my mlspec
project, which currently
generates entire Haskell projects (Cabal files and all) in order to
avoid hand-coding theories for QuickSpec. All
of that shenanigans should be replacable by a simple call out to
eval
, and even better I should be able to simply discard
failures, rather than have GHC flat out refuse to touch anything else in
the project.
There are still tricky issues like running nix-shell
s
inside nix-shell
s. For example, this page’s source is a
little complicated since the Haskell code is running
nix-shell
(via nix-eval
); yet that Haskell
code itself is running in nix-shell
, in order to
satisfy the nix-eval
dependency; and all of this is inside
another nix-shell
which is rendering my site.
That’s required a couple of tricks, eg. using nix-shell
shebangs rather than direct invocations from the shell; and using
nix-shell
to build the required GHC + packages,
but actually invoking it from outside the shell.
Hopefully the edge-cases will become less painful as Nix evolves :)