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 reasons

However, 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 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
main = do result <- eval (asString "hello world")
          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

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 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-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 :)