tweag / nickel

Better configuration for less
https://nickel-lang.org/
MIT License
2.25k stars 84 forks source link

Default values in dictionary contracts #1336

Open YorikSar opened 1 year ago

YorikSar commented 1 year ago

In nickel-nix, we specify inputs with a relevant portion of the contract like this:

let inputs_spec = {
  nix.input | default = "nixpkgs",
  git.input | default = "nixpkgs",
}
in
inputs_spec | { _ | { input | String } }

"nixpkgs" is an obvious common default for the input, so if we move this default to the contract, we can make it look better:

let inputs_spec = {
  nix = {},
  git = {},
}
in
inputs_spec | { _ | { input | String | default = "nixpkgs"} }

In this case user still has to know to add = {} to every input. I want to be able to specify a default value for keys without values in the dictionary, so that we can shorten it further. It would look something like this:

let inputs_spec = {
  nix,
  git,
}
in
inputs_spec | { _ | { input | String | default = "nixpkgs"} | default = {} }

(it would look more like {_ | NickelInputSpec | default = {} }, not with 2 defaults in one row, of course)

Note that this works just fine in a record contract:

nickel> {x} | { x | { input | String | default = "nixpkgs"} | default = {} }
{ x = { input = "nixpkgs" } }

but doesn't work if we replace x with _. I would expect other people to be confused about this difference as well.

shlevy commented 1 year ago

I would like this as well. Use case is: Configuration files specifies a set of outputs as a dict, where the key is the output name and the value is a record specifying the output... But all of the fields have defaults, so would be great to have a way to make { foo, bar = { ... } } equivalent to {foo = {}, bar = { ... } }

yannham commented 1 year ago

At first sight, this seems reasonable. Dictionary contracts behave exactly as if the were attached to each and every field of the record, that is:

{foo = ..., bar = ...} | {_ | Contract} ~ {foo | Contract = ..., bar | Contract = ...}

Under this interpretation, it's natural to extend the annotation attached to _ to be any field metadata, and not just a contract (or, if we want to be safe, we can just add default values for now, and extend annotations further as users people request it - maybe there's one or more metadata whose "dictionary" semantics is not as clear or has some kind of non-obvious issues).

yannham commented 1 year ago

With the caveat that the record to which the dictionary contract is applied could already have default values (or other kind of metadata). However, we can already hit this case with plain old record contracts:

{ foo | default = 1} | {foo | Number | default = 2}

As a rule, we can always stick to the "dictionary contracts are expanding like a record contract but for every field" interpretation, and thus simply have the same behavior as record contracts. In the case of default values, for example, the values are merged.

vkleen commented 1 year ago

Just from a syntax perspective, this makes me wonder if we would want to permit { _ = ... } as a dictionary contract. For normal records, a default value is just a normal field assignment together with the | default metadata annotation. For dictionary contracts, we could do the same and it wouldn't be a breaking change, since for example {_ = 5} is currently a parse error.

yannham commented 1 year ago

I propose to move forward by first adding ad-hoc support for default values. That is, in a way that would be forward-compatible with having dictionary contracts where _ is just like any field definition, and can get assigned a value, other metadata, etc., but without implementing those yet. The syntax is consistent with record literals and the rest of the language.