Nickel: the Nix (language) spin-off

For those who attended NixCon on Saturday, I presented in a lightening talk an on-going project called Nickel. This is a configuration programming language inspired from Nix. It is still not at release stage, but if you want to learn more, there’s a fresh blog post about it, and we recently made the repo public.

Cheers

P.S: don’t mind the current ugly examples in src/examples, they are quite old and need to be rewritten once we’ve made a pass on the syntax.

11 Likes

Knew it would be in Rust even before opening the link :slight_smile:

I followed Théophane‘s work on gradually typing nix - looks like this is some follow-up?

The details may have changed a bit since Théophane’s initial report, but yes indeed, this is somehow the continuation of this approach.

Are there any examples on how one would use Nickel to create a Nix derivation? If I understand it correctly, Nix has the derivation primitive, and the Nixpkgs standard library provides a whole bunch of extra function (e.g., the mkDerivation convenience function around derivation). Would it be possible to use the stdenv functions, or would these have to be re-implemented in Nickel?

1 Like

This is a very good question, and we are exploring this at this very moment.

Currently, we are experimenting with the least invasive and simplest alternatives, that would allow you to write just one derivation or one NixOS module in Nickel, without having to implement implement stuff either in Nix (I mean, in the package manager itself) or to re-implement the Nix features you mention on the Nickel side.

For example, you could write a shell description in Nickel (shell.nickel). Then, your shell.nix calls to a generic Nix library function that runs Nickel to generate a JSON, then imports said JSON and return the corresponding shell. This is not totally as simple as it sounds and a bit hacky (just specifying buildInputs can’t be done just in JSON, so the Nix import code actually has to do some parsing to rebuild the inputs), but is done easily in a very direct way.

Another way would be to have good languages interoperability, for example being able to import nix expressions directly in Nickel and to transpile Nickel values (once evaluated) to Nix expressions. Doing so, we can still leverage everything that already exists in Nix without having to re-implement it on the Nickel side.

Lastly, the most involved ones - but maybe also the most powerful - would be to either add Nickel support directly in Nix, which would accept Nickel sources in addition to Nix expressions, or the other way around, as you described: re-implement Nix features on the Nickel side such that a Nickel file would be able to evaluate all the way down to a derivation.

9 Likes

This is super exciting and I appreciate the detailed thoughts about this. While unsurprising that thought has been put into this, it’s great to see confirmation and start to imagine.

3 Likes

Thank you for the detailed answer! By the way, when I first skimmed the introduction to Nickel and saw gradual typing mentioned, I thought that Nickel’s relationship to Nix will be akin to Typescript’s relationship to JavaScript (as in, a valid JS program is a valid TS program as well), but you already answered this as well:)

1 Like

Glad to hear that!

You’re welcome!

It’s true that most of gradually typed languages are usually a superset of an existing, dynamically typed language. However, in the case of Nickel, we chose gradual typing on its own: you can learn more about this in this blog post on Nickel’s type system.

Additionally, Nickel won’t be a strict superset of Nix, at least syntactically, for practical reasons: for example, we want to use : for type annotations as in any other language out there, but Nix uses it for function arguments.

That said, Nickel could be close to a superset of Nix, modulo syntax: parsing Nix expressions and translating them to Nickel does seem reasonable, although there’s still some aspects to figure out (Nickel currently doesn’t have string contexts, for example, neither dynamic scoping via with).

2 Likes

Just found the Figure out an adoption plan #93 issue in the Nickel repo that provides some really interesting facts. For example,

Nix already supports importing other languages, via the import-from-derivation feature. If the language can produce nix expressions in a derivation, nix will be able to import them as if they were provided to the interpreter directly (they are loaded from a store path). ()

but the entire thread is worth reading. (It also answers a couple other questions that I now don’t have to ask.)

3 Likes

One of pain points about Nix due to interpreted nature of it is that with complex code it becomes somewhat slow. Is there a plan to use typing system to improve its performance?

This GitHub repo is a very instructive one to watch, imo. I really like that everything is done out in the open, and how the rationale for every design decision is extremely explicit.

I also kinda like that the implementer is somewhat an ‘outsider’ to Nix who has done research on it, along with lots of other configuration languages, who tries to make really really clear what he aims to do, and then seeks approval from folks who have worked a lot on Nix and Nixpkgs.

The detailed way that every decision is documented and considered, even when there may be an ‘obvious’ general direction forward for the semantics of a given feature (e.g., act like Nix/Nixpkgs) is very thought-provoking for me.

I also think the pace of development is kinda impressive; once yannham has determined what is desired he seems to know how to go about it and get right to it.

Anyway idk if anyone else does recreational GitHub browsing now and again, but the Nickel repo is great for it

3 Likes

One of pain points about Nix due to interpreted nature of it is that with complex code it becomes somewhat slow. Is there a plan to use typing system to improve its performance?

Do you have specific optimizations in mind? I’m not aware of really impactful type-driven optimizations for dynamic languages, in particular in the case of configuration, where we shouldn’t expect e.g. heavy numbers crunching. Unless you mean going down the compiling to bytecode route where, indeed, static typing can help generate more specialized code, but this is a whole different thing and we are not there yet.

I think one domain where there is potential for performance is the fact that Nickel tries to take the widespread and ubiquitous problems that Nix users encounter, and to provide language features to handle these problems natively. I’m thinking about overriding (Nickel’s merge system) or the dynamic typechecking of NixOS modules (Nickel’s contracts) for example. In Nix, these features are implemented in library code, which limits what can be done to apply specific optimizations. On the other hand, in Nickel, these are built in the language - and thus, in the interpreter - which gives in principle more room for low-level, specific optimizations.

Another point is parallelism: one design goal is to keep the language easily parallelizable. This is taken into account when adding language features.

1 Like

Let me know if you want me to open an issue for this:

RFC92 introduces a language feature that enables “time travel”: basically, it is a mechanism of evaluation deference for “expensive” evaluations, for example those that require to process upstream external tooling.

I think this line of thinking can be interesting for Nickel development, although I don’t have any concrete idea or suggestion.

Stumbling over my own sins, I suggest inclusion of an “executable” docstring format that doubles up as a unit test (nkl --check or something).

As a functional language, this should be conveniently easy to reason about (no setups or tear downs).

{
/ **
[Synopsis]

Input:
  {
    arg = "string";
  }
  --
  {
    arg = "strung";
  }
  

Output:
  {
    arg = "string-string";
  }
  --
  {
    arg = "string-strung"; # Ha! You thought otherwise ;-)
  }
**/
myFuction: s ...
}
1 Like

Another thing: through a nickel codestring canonicalizer (or similar), it should be possible to implement a semantic differ.

Semantic differs are extremely convenient in situation where copy-pasta an upstream is a superior strategy to implementing an override.

In case of doubt, such superiority is given if value code readability > value of ease of incorporating upstream changes through repinning.

Here, a semantic differ fundamentally lowers the value of ease of incorporating upstream changes through repinning and hence unlocks value of code readability in some cases.

Such as nkl diff a.nkl b.nkl could result in:

- a.b.c = "foo";
+ a.b.c = "bar";

A code string canonicalizer also helps for string hashing of the type that RFC92 suggests.

@yannham I would be interested to read about the advantages of Nickel over Cue which inspired it.

Interestingly enough, we discussed effects in Nickel last week with @regnat and others. The issue is how to handle use-cases where some values are determined only once an effectful operation (say, call to an external tool) have been performed. I haven’t taken time to read RFC92 in all details, but I think we came to a probably not-too-different conclusion: in all generality, we must be able to handle and combine values with deferred computations inside, effectively building an abstract syntax tree (where the available parts are still evaluated as much as possible), that can be realized later.

1 Like

You can refer to the RATIONALE document, and more specifically, to the section comparison with CUE.

1 Like

The only difference mentioned is

it seems less adapted to generating configuration in general. It is also heavily constrained, which might be limiting for specific use-cases.

but I’m afraid that doesn’t clarify much for me. The goal of CUE is configuration, so I don’t see how it’s less adapted to doing so.