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.