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:
main = putStr "hello world"TODO: Disabled for space reasonsHowever, if we try to write "hello world" using the
popular Text library, it breaks:
main = putStr (Data.Text.unpack (Data.Text.pack "hello world"))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
main = putStr (Data.Text.unpack (Data.Text.pack "hello world"))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
main = putStr (Data.Text.unpack (Data.Text.pack "hello world"))TODO: Disabled for space reasonsHowever, 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
main = do result <- eval (asString "hello world")
case result of
Nothing -> error "Didn't work :("
Just x -> putStr xTODO: Disabled for space reasonsAs 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
dt = withPkgs ["text"] . qualified "Data.Text"
main = do result <- eval (dt "unpack" $$ (dt "pack" $$ asString "hello world"))
case result of
Nothing -> error "Didn't work :("
Just x -> putStr xTODO: Disabled for space reasonsnix-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-shells
inside nix-shells. 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 :)