Hacker News new | ask | show | jobs
by cogman10 2062 days ago
Yeah... I honestly don't see the appeal of nickel.

It is sold as "You use this to generate configuration in other formats like JSON"... but why? Why would I want to use some language other than the target format to configure things? Why am I making my configuration a 2 step process? And even if I bought all of those reasons, why wouldn't I just use a general purpose language instead? Why have some esoteric language dialect whose only purpose is... making configuration files?

I'd much rather use Bash, python, perl, javascript, typescript, groovy, Java, kotlin, C++, C, Rust, erlang, php, awk, pascal, go, Nim, Nix, VB, Hax, coffeescript etc. Really, take your pick. Any well established language seems like a much better approach than something like this.

3 comments

Here's a snippet for configuring a systemd timer on NixOS. Note that if I were to use the systemd configuration language, it would be spread across two files (the timer and the service itself)[1]. If I don't have "startAt" in the definition, it won't generate the timer file. If I spell it "statrAt" it will give me an error when I generate it (or in my editor if I have that configured). Note it's possible to fallback on using the json-like syntax to generate the ini-like systemd configuration files manually, which is useful to have when needed, but mostly it's about writing fairly simple functions that increase the signal-to-noise of the configuration file by removing boilerplate while at the same time detecting mistakes earlier.

  systemd.services.tarsnapback  = {
    startAt = "*-*-* 05:20:00";
    path = [ pkgs.coreutils ];
    environment = {
      HOME = "/home/XXXX";
    };
    script = ''${pkgs.tarsnap}/bin/tarsnap -c -f "$(uname -n)-$(date +%Y-%m-%d_%H-%M-%S)" "$HOME/ts" '';
    serviceConfig.User = "XXXX";
  };
1: Quick reference if you aren't familiar with systemd timers: https://wiki.archlinux.org/index.php/Systemd/Timers
I would 100% choose to write a systemd foo.timer file, and the foo.service file, and reference those.

You're throwing away all the organizational learning and preexisting systemd documentation, and forcing something different on the world. `man systemd.timer` contains no mention of `startAt`; what you have there is something inherently different from systemd.

And what if I want more complex rules, like a combination of intervals and time from boot?

> I would 100% choose to write a systemd foo.timer file, and the foo.service file, and reference those.

NixOS gives you this option, and I choose not to. Fortunately nobody is forcing you to use this (or forcing me to not use it).

> You're throwing away all the organizational learning and preexisting systemd documentation, and forcing something different on the world. `man systemd.timer` contains no mention of `startAt`

Not quite throwing it all away, because you can easily observe the output of this before making it live. Yes, systemd.timer contains no mention of startAt because as you correctly observed this is somethign inherently different from systemd. startAt is used by other configuration options to specify items running at specific calendar times, so it's reasonably consistent within nixOS itself.

To read the nix documentation is quite simple (and it shows the currently configured value for you):

  % nixos-option systemd.services.tarsnapback.startAt
    Value:
    [ "*-*-* 05:20:00" ]

    Default:
    [ ]

    Type:
    "string or list of strings"

    Example:
    "Sun 14:00:00"

    Description:
    ''
      Automatically start this unit at the given date/time, which
      must be in the format described in
      <citerefentry><refentrytitle>systemd.time</refentrytitle>
      <manvolnum>7</manvolnum></citerefentry>.  This is equivalent
      to adding a corresponding timer unit with
      <option>OnCalendar</option> set to the value given here.
    ''
> what you have there is something inherently different from systemd.

That's kind of the point. If it was inherently the same as systemd there would be no point to it. Systemd timers are quite boilerplate heavy (compare to e.g. a crontab entry), so when I'm not using nixos, I often end up copying an existing timer and modifying it.

> And what if I want more complex rules, like a combination of intervals and time from boot?

Add a time from boot of 120 seconds with this:

  systemd.timers.tarsnapBack.timerConfig = { OnBootSec = "120"; };
For things that actually use all the bells and whistles of systemd, you'll need to specify all the various details.

[edit]

For a nice hyperlinked searching of options see also:

https://search.nixos.org/options?query=startAt&from=0&size=3...

Three things to address:

1) This doesn't have to be a two-step process. Specialized tools like kubecfg for Jsonnet will directly take a Jsonnet top-level config and instantiate it, traverse the tree, and apply the configuration intelligently to your Kubernetes Cluster.

2) General purpose languages are at a disadvantage, because most of them are impure. Languages that limit all filesystem imports to be local to a repository and disallow any I/O ensure that you can safely instantiate configuration on CI hosts, in production programs, etc. The fact that languages like Jsonnet also ship as a single binary (or simple library) that requires no environment setup, etc. also make them super easy to integrate to any stack.

3) Configuration languages tend to be functional, lazily evaluated and declarative, vastly simplifying building abstractions that feel more in-line with your data. This allows for progressive building of abstraction, from just a raw data representation, through removal of repeated fields, to anything you could imagine makes sense for your application.

Related reading: https://landing.google.com/sre/workbook/chapters/configurati...

I don’t think they tend to be lazily evaluated (unless you mean “lazy” in some other way than I’m familiar with), but in general I agree.
Jsonnet, Nix and CUE are lazily evaluated. Starlark is not IIRC. Dhall I don't know, but I would presume it is?

Nix as an example:

  nix-repl> { foo = 5 / 0; bar = 5; }    
  error: division by zero, at (string):1:9

  nix-repl> { foo = 5 / 0; bar = 5; }.bar 
  5
vs. Python as an obvious example of a language with eager evaluation:

  >>> { "foo": 5 / 0, "bar": 5 }.bar
  Traceback (most recent call last):
    File "<stdin>", line 1, in <module>
  ZeroDivisionError: division by zero
This lazy evaluation allows for a very nice construct in Jsonnet:

  local widget = {
    id:: error "id must be set",
    name: "widget-%d" % [self.id],
    url: "https://factory.com/widget/%d" % [self.id],
  };
  widgetStandard: widget { id: 42 },
  widgetSpecial: widget { name: "foo"; url: "https://foo.com" },
When the resulting code only expects a widget to have a 'name' and 'url' field, you can either have both automatically defined based on a single to-level ID, or override them, even fully skipping the ID if not needed. (a :: in jsonnet is a hidden field, ie. one that will not be evaluated automatically when generating a YAML/JSON/..., but can be evaluated by other means).
JSON and YAML don’t offer any abstraction. If you want to describe kubernetes resources in such a way that you can deploy the same resources to many environments with subtle differences between them (e.g., namespace names, DNS names, etc) you want something to let you abstract so you aren’t manually trying to keep disparate copies of thousands of lines of frequently-changing config in sync.

The reason you don’t use regular languages for this task is because you want to enforce termination (programs can’t run forever without halting, allowing someone to DoS your system) or reproducibility (the config program doesn’t evaluate to different JSON depending on some outside state because the program did I/O). If your use case involves users who can be trusted not to violate these principles, then a standard programming language can work fine, but this frequently isn’t the case.

> you want to enforce termination

Nickel is turing complete. See, the fib example.

> or reproducibility

Nickel doesn't force reproducibility

So again, why Nickel and not a GP programming language?

Sorry, my reply, like throwaway's missed the main point of your original comment of "why not a GP programming language"

A configuration file is uniquely suited to a pure and lazy language.

Pure, because the all the advantages of a pure language remain, while none of the downsides; the result of evaluating the function is your configuration data. You don't need to do arbitrary I/O and ordering for generating configuration files.

Lazy because configuration files are naturally declarative, but you don't want to evaluate tons of things you have declared but then never used.

> Nickel is turing complete. See, the fib example.

I should have been more clear, I was listing potential reasons why you might not use a standard programming language. "Not wanting turing completeness" is a reason to use a non-turing-complete DSL. I wasn't suggesting that Nickel was appropriate for this particular use case, but many of the other languages in this category are (e.g., Starlark, Dhall).

> Nickel doesn't force reproducibility

Scanning the docs, I don't see anything about Nickel allowing I/O, so I believe you're mistaken.

https://github.com/tweag/nickel/blob/master/RATIONALE.md

> However, sometimes the situation does not fit in a rigid framework: as for Turing-completeness, there may be cases which mandates side-effects. An example is when writing Terraform configurations, some external values (an IP) used somewhere in the configuration may only be known once another part of the configuration has been evaluated and executed (deploying machines, in this context). Reading this IP is a side-effect, even if not called so in Terraform's terminology.

This is the relevant passage:

> Nickel permits side-effects, but they are heavily constrained: they must be commutative, a property which makes them not hurting parallelizability. They are extensible, meaning that third-party may define new effects and implement externally the associated effect handlers in order to customize Nickel for specific use-cases.

This answers your question about why Nickel is preferable to general purpose programming languages--the side-effects are more limited. Further, it reads to me like the "side-effects" are something that the owner of the runtime opts into by extending the sandbox with callables that can do side-effects as opposed to untrusted code being able to perform side-effects in any Nickel sandbox.

Hi, blog post author here. The idea behind effects in Nickel is to have very limited, use-case specific effects that can extend the standard interpreter. The goal is, as the example suggest, to make it able to interoperate with an external tool when absolutely necessary, such as Terraform or Nix. The idea is really not to have general effectful functions such as readFile or launchMissiles.
It's not clear from the docs whether any Nickel program can perform side-effects or if the Nickel interpreter must be extended to allow programs to perform side-effects (a la Starlark). Can you clarify this point?