hnix/README-design.md

2.7 KiB

Design of the hnix code base

Welcome to the hnix code! You may notice some strange things as you venture into this realm, so this document is meant to prepare you, dear reader, for the secrets of the labyrinth as we've designed them.

The first thing to note is that hnix was primarily designed so that Haskell authors could use it to craft custom tooling around the Nix ecosystem. Thus, it was never fully intended for just the end user. As a result, we use a great deal of abstraction so that these enterprising Haskell authors may inject their own behavior at various points within the type hierarchy, the value representation, and the behavior of the evaluator.

To this end, you'll see a lot of type variables floating around, almost everywhere. These provide many of the "injection points" mentioned above. There is a strict convention followed for the naming of these variables, the lexicon for which is stated here.

t is the type of thunks. It turns out that hnix dosen't actually need to know how thunks are represented, at all. It only needs to know that the interface can be honored: pending action that yield values may be turned into thunks, and thunks can later be forced into values.

f is the type of a comonadic and applicative functor that gets injected at every level of a value's recursive structure. In the standard evaluation scheme, this is used to provide "provenance" information to track which expression context a value originated from (e.g., this 10 came from that expression "5 + 5" over in this file, here).

m is the "monad of evaluation", which must support at least the features required by the evaluator. The fact that the user can evaluate in his own base monad makes it possible to create custom builtins that make use of arbitrary effects.

v is the type of values, which is almost always going to be NValue t f m, though it could be NValueNF t f m, the type of normal form values. Very few points in the code are generic over both.

Different value types

Having said that, I should mention that there are two different types of values: NValue and NValueNF. The former is created by evaluating an NExpr, and then latter by calling normalForm on an NValue.

However, not every term can be reduced to normal form. There are cases where Nix allows a cycle to exist in the data, so that it can printed simply as <CYCLE>. To represent this, we use a simple recursive type for NValue, but a Free construction for NValueNF:

type NValueNF t f m = Free (NValue' t f m) t

The idea here is that Free values are those we were able to normalize (since it has its own terminating base cases of constants, strings, etc), while the Pure thunk is the thunk we'd seen before while normalizing, indicating the beginning of the cycle.