chriswarbo-net: e8965c13d14ece7ba929e2af8b4e35cece63b970

     1: ---
     2: title: nix-eval for Haskell
     3: packages: [ 'nix-shell' ]
     4: dependencies: [ 'static/nix' ]
     5: sha256: "sha256-YQOCn/1STyI5BiSUDcmPdeY7WCp/rZasHLwT2PzBzGI="
     6: ---
     7: 
     8: The [`nix-eval` package](https://hackage.haskell.org/package/nix-eval) scratches
     9: a particular itch for me. It allows Haskell code to be evaluated at run-time, in
    10: a sub-process, using [Nix](http://nixos.org/nix) to get any dependencies it
    11: needs.
    12: 
    13: NOTE: This page is [active code](/projects/activecode/), so check out the "view
    14: source" link at the bottom of the page if you want to follow along with the
    15: examples!
    16: 
    17: ```{pipe="cat > nixpkgs.nix"}
    18: fetchTarball {
    19:   sha256 = "1l4hdxwcqv5izxcgv3v4njq99yai8v38wx7v02v5sd96g7jj2i8f";
    20:   url    = "https://github.com/nixos/nixpkgs/archive/"
    21:     + "94d80eb72474bf8243b841058ce45eac2b163943.tar.gz";
    22: }
    23: ```
    24: 
    25: ```{pipe="cat > hsPkgs.nix"}
    26: with { nixpkgs1803 = import ./nixpkgs.nix; };
    27: with import nixpkgs1803 {};
    28: (haskell.packages.ghc7103.override (old: {
    29:   overrides = lib.composeExtensions
    30:     (old.overrides or (_: _: {}))
    31:     (self: super:
    32:       with {
    33:         spr = lib.mapAttrs (n: v: self.callHackage n v {}) {
    34:           # Required by mlspec
    35:           haskell-src-exts = "1.17.1";
    36: 
    37:           # Required by nix-eval, newer versions conflict with haskell-src-exts 1.17
    38:           hindent = "4.6.4";
    39: 
    40:           # QuickCheck 2.10+ breaks quickspec 1.x
    41:           QuickCheck = "2.9.2";
    42: 
    43:           # Nixpkgs version doesn't support QuickCheck < 2.10
    44:           quickcheck-instances = "0.3.14";
    45: 
    46:           # Force version 1 of quickspec, since nixpkgs may be using 2.x
    47:           quickspec = "0.9.6";
    48: 
    49:           # Needed for text-short
    50:           tasty-quickcheck = "0.8.4";
    51: 
    52:           # Nixpkgs version has incorrect dependencies, presumably due to being built
    53:           # against GHC 8 rather than 7
    54:           text-short = "0.1.1";
    55: 
    56:           # Nixpkgs version is missing semigroups dependency
    57:           transformers-compat = "0.5.1.4";
    58:         };
    59:       };
    60:       spr // {
    61:         # Missing Arbitrary instances
    62:         aeson = haskell.lib.dontCheck super.aeson;
    63: 
    64:         # Archive is missing files needed for tests
    65:         hindent = haskell.lib.dontCheck spr.hindent;
    66: 
    67:         # This seems to be needed for nix-eval to use our overrides
    68:         nix-eval = super.nix-eval.override { inherit (self) hindent; };
    69:       });
    70: })).ghcWithPackages
    71: ```
    72: 
    73: ```{pipe="cat > runWithPkgs.sh"}
    74: #!/usr/bin/env bash
    75: set -e
    76: 
    77: echo "WARNING: Skipping nix_eval execution for space reasons" 1>&2
    78: echo "TODO: Disabled for space reasons"
    79: exit 0
    80: 
    81: export HOME="$PWD"
    82: 
    83: # Separate all arguments with spaces
    84: PKGS=""
    85: for PKG in "$@"
    86: do
    87:   PKGS="$PKGS $PKG"
    88:   echo "Using pkg '$PKG'" 1>&2
    89:   PKG=""
    90: done
    91: 
    92: {
    93:   echo 'with { inherit (import root/static/nix {}) nixpkgs; };'
    94:   echo 'nixpkgs.mkShell {'
    95:   echo '  name = "nix_eval-shell";'
    96:   echo '  packages = ['
    97:   echo '    which'
    98:   echo "    (import ./hsPkgs.nix (h: [$PKGS]))"
    99:   echo '  ];'
   100:   echo '}'
   101: } > shell.nix
   102: 
   103: command -v nix-shell 1>&2
   104: 
   105: CMD=$(nix-shell --store "$HOME" --show-trace --run 'which runhaskell')
   106: echo "Running '$CMD'" 1>&2
   107: if "$CMD" -XOverloadedStrings
   108: then
   109:   rm -f shell.nix
   110: else
   111:   CODE="$?"
   112:   rm -f shell.nix
   113:   exit "$CODE"
   114: fi
   115: ```
   116: 
   117: ```{pipe="sh > /dev/null"}
   118: chmod +x runWithPkgs.sh
   119: (source "$stdenv/setup" && patchShebangs .)
   120: ```
   121: 
   122: As tradition dictates, we'll start with `"hello world"`, which is trivial to do
   123: using Haskell's built-in `String` type:
   124: 
   125: ```{.haskell pipe="tee hws.hs"}
   126: main = putStr "hello world"
   127: ```
   128: 
   129: ```{.haskell pipe="sh"}
   130: ./runWithPkgs.sh < hws.hs
   131: ```
   132: 
   133: However, if we try to write `"hello world"` using the popular `Text` library, it
   134: breaks:
   135: 
   136: ```{.haskell pipe="tee hwt.hs"}
   137: main = putStr (Data.Text.unpack (Data.Text.pack "hello world"))
   138: ```
   139: 
   140: ```{pipe="sh"}
   141: ./runWithPkgs.sh < hwt.hs 2>&1
   142: exit 0 # Ignore the error
   143: ```
   144: 
   145: In order to use functions like `Data.Text.unpack`, we first need to import the
   146: `Data.Text` module:
   147: 
   148: ```{.haskell pipe="tee import.hs"}
   149: import Data.Text
   150: main = putStr (Data.Text.unpack (Data.Text.pack "hello world"))
   151: ```
   152: 
   153: ```{pipe="sh"}
   154: ./runWithPkgs.sh < import.hs 2>&1
   155: exit 0 # Ignore the error
   156: ```
   157: 
   158: Another error! In order to import modules like `Data.Text`, we first need to
   159: register the `text` package. This can't actually be done from within Haskell; we
   160: need to perform this step before invoking the Haskell compiler/interpreter.
   161: 
   162: Most of the time that's fine, and we have tools like Cabal and Nix to help us:
   163: 
   164: ```{.haskell pipe="sh"}
   165: cat import.hs
   166: ```
   167: 
   168: ```{.haskell pipe="sh"}
   169: ./runWithPkgs.sh "h.text" < import.hs
   170: ```
   171: 
   172: However, this falls apart when we want to evaluate Haskell code at *run-time*,
   173: ie. using an `eval` function.
   174: 
   175: Run-time code evaluation is well-known in scripting languages, and although it's
   176: a little tricky, it can be done in Haskell too. However, most implementations of
   177: `eval` rely on the packages and modules which are available to the "host"
   178: program, rather than what is needed by the given expression.
   179: 
   180: That's where `nix-eval` comes in. It provides a simple `Expr` data type which
   181: contains a `String` of code to evaluate, along with a list of packages and
   182: modules it needs. During evaluation, these dependencies are fetched using Nix,
   183: and the code is sent to a new instance of GHC, which has those packages
   184: available.
   185: 
   186: For example, our simple `"hello world"` would become:
   187: 
   188: ```{.haskell pipe="tee nixs.hs"}
   189: import Language.Eval
   190: main = do result <- eval (asString "hello world")
   191:           case result of
   192:                Nothing -> error "Didn't work :("
   193:                Just x  -> putStr x
   194: ```
   195: 
   196: ```{.haskell pipe="sh"}
   197: ./runWithPkgs.sh "h.nix-eval" < nixs.hs
   198: ```
   199: 
   200: As you can see, it's a bit more verbose. In particular, the output type of
   201: `eval` is `IO (Maybe String)`, since evaluation is not a pure function (`IO`)
   202: and it may fail (`Maybe`). The `String` is the standard output of the GHC
   203: process which Nix invokes, which we're free to parse in any way we like.
   204: 
   205: Next we can try the `text` example, but this time we won't import or register
   206: anything besides `nix-eval`, like above:
   207: 
   208: ```{.haskell pipe="tee nixt.hs"}
   209: import Language.Eval
   210: 
   211: dt = withPkgs ["text"] . qualified "Data.Text"
   212: 
   213: main = do result <- eval (dt "unpack" $$ (dt "pack" $$ asString "hello world"))
   214:           case result of
   215:                Nothing -> error "Didn't work :("
   216:                Just x  -> putStr x
   217: ```
   218: 
   219: ```{.haskell pipe="sh"}
   220: ./runWithPkgs.sh "h.nix-eval" < nixt.hs
   221: ```
   222: 
   223: `nix-eval` has a few simple combinators, including `$$` which applies one `Expr`
   224: to another (like Haskell's `$`); `withPkgs` and `withMods` which append to an
   225: `Expr`'s context; and `qualified` which appends to the context *and* prefixes
   226: the expression.
   227: 
   228: I wrote this to help simplify my [`mlspec`](/git/mlspec/)
   229: project, which currently generates entire Haskell projects (Cabal files and all)
   230: in order to avoid hand-coding theories for
   231: [QuickSpec](https://hackage.haskell.org/package/quickspec). All of that
   232: shenanigans should be replacable by a simple call out to `eval`, and even better
   233: I should be able to simply discard failures, rather than have GHC flat out
   234: refuse to touch anything else in the project.
   235: 
   236: There are still tricky issues like running `nix-shell`s inside `nix-shell`s. For
   237: example, this page's source is a little complicated since the Haskell code is
   238: running `nix-shell` (via `nix-eval`); yet that Haskell code *itself* is running
   239: in `nix-shell`, in order to satisfy the `nix-eval` dependency; and all of this
   240: is inside another `nix-shell` which is rendering my site.
   241: 
   242: That's required a couple of tricks, eg. using
   243: [`nix-shell` shebangs](/projects/nixos/nix_shell_shebangs.html) rather than direct
   244: invocations from the shell; and using `nix-shell` to *build* the required GHC +
   245: packages, but actually *invoking* it from outside the shell.
   246: 
   247: Hopefully the edge-cases will become less painful as Nix evolves :)

Generated by git2html.