tweag / nickel

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

Field punning (inherit) #747

Open yannham opened 2 years ago

yannham commented 2 years ago

Inherit

Nix has an inherit keyword that is used to define a field with the same name and value as a a variable in scope:

let foo = 1; in
{ inherit foo; }

# is equivalent to
{ foo = foo; }

This kind of syntactic sugar is also present in other languages (Haskell, Rust, OCaml, etc.) sometimes under the name record /field punning.

Nix inherit also supports multiple fields, and the specification of another record to take the fields from:

let record = {foo = 1; bar = 2;}; in
let baz = 3; blarg = 4; in

{
  inherit (record) foo bar;
  inherit baz blarg;
}

# is equivalent to
{
  foo = record.foo;
  bar = record.bar;
  baz = baz;
  blarg = blarg;
}

Recursive records in Nickel

While this is a nice shorthand, inherit is at most a nicety. However, in Nickel, records are recursive by default. This means that the expanded version above will just cause an infinite loop:

{foo = foo}

This make defining fields with the same name and content as an identifier in scope a pain: one either has to change the variable from the beginning:

let foo_ = 1 in
let bar_ = 2 in

{
  foo = foo_,
  bar = bar_,
}

But sometimes that is not desirable. Variables can be defined far away, and can be used in a lot of other places where it makes sense to keep the original name. The only solution left is to bind those variables to new ones locally, but this must be done before the definition of the record:

let foo = 1 in
let bar = 2 in

{
  sub_record =
    let foo = foo_ in
    let bar = bar_ in
    {
       foo = foo_,
       bar = bar_,
     }
}

Another case that I encountered in practice is when you need to recursively refer to a field defined in a parent record, but with the same name. There, you can't rename the original identifier either:

{
  meta = "metadata",
  sub = let meta_ = meta in {
    meta = meta_
  }
}

This starts to be really painful. Because records are recursive by default, I think having field punning is even more important for Nickel than for other languages.

Describe the solution you'd like

Have a syntactic construct, which can be inherit or something else, to define fields with the same name and value as a list of identifiers in scope.

Note that the common punning syntax for several languages (Rust, OCaml, etc.) is to just write a field without a definition. For example in OCaml:

type r = {foo : int}
let foo = 1 in {foo;}

Or in Rust:

struct Foo { bar: i32 }

fn mk_foo() -> Foo {
  let bar = 1;
  Foo {bar}
}

But this solution doesn't cope well with the general syntax of Nickel.

{
  foo | Num = 1, # a field with a contract and a definition
  foo | Num, # a field with a contract but without definition
  foo, # a field without a definition
}

a lone field named foo, is taken to be a field without definitions, and this is consistent with other notations. Beside being a breaking change, I don't think that would be intuitive to change this syntax to mean inherit foo.

francois-caddet commented 2 years ago

As discussed during last nickel meeting, keeping the Nix syntax ({ inherit var, ...}) is probably better. @yannham also proposed to have a more intuitive syntax for inherit(record) var which is inherit var from record. I agree with this proposal, but also feel it less a requirement for nickel than the simple form inherit var which as said above, is the only way to write something meaning let a = something in {a = a} in nickel because of records recursivity.

toastal commented 1 year ago

PureScript, JavaScript, et.al. would let you use the variable as a label such that { foo }{ foo = foo } (though I think inherit ideas offer more flexibility)

yannham commented 1 year ago

@toastal, the end of the original post covers this case and explains why this wouldn't be optimal.

We have a notion of fields without a definition, and in general something that could be described as "partial configurations". Currently, {foo} is already a valid value, which is a record with a field foo that doesn't have yet a definition (which can be seen as a contract as well, which simply requires the existence of a field foo). While the {foo} ~ {foo = foo} is a natural and reasonable approach in other programming languages, that wouldn't work as well in Nickel, unfortunately.

olorin37 commented 4 months ago

Yeah, it is indeed very unfortunate that undefined field notation is the same as punning could be. It would be very natural even for people comming from js, where you can write: const some_field = 7; const an_object = { some_field } and it is very handy... That's petty this opportunity has been lost (for making Nickel familiar for mainstream, in this case).

On the other side I think it would be good to avoid inherit keyword, it is of course sensible (especially comming from nix, but not ony), but:

I have a bunch of ideas:

What do you think? @yannham? I do not know if those examles are not in collision with other syntax of Nickel, didn't analyse it, are they?

yannham commented 4 months ago

I personally prefer keyword-based solutions, because they don't assume that the reader of Nickel code is familiar with C-like languages, with JavaScript, with functional programming or whatnot. Although it does have a cost: it's more verbose, and it sometimes prevents said keywords from being used as an identifier (which is backward incompatible). That being said, in this case, because the syntax of field names is very restricted, it's probably never ambiguous (that is, reserving a keyword like use or add in this position doesn't prevent from using it as an identifier elsewhere, because it never clashes).

I'm not too worried about the size of inherit, but the unnecessary association with OOP is a good point. add is confusing, because it makes you think of number addition. put is short and nice, but has an "imperative" feeling IMHO - like we are mutating something, which we aren't. use is a good one; we just have to be careful if one day we want to also have something like Nix with - but more principled, as described in this blog post - because use would be a good candidate for that as well, and I'm not sure the two would coincide.

Naming is hard :upside_down_face: we can also add include to the possibilities

toastal commented 4 months ago

Is OOP still as pervasive as it was in the ’90s & ’00s?

olorin37 commented 4 months ago

Yep, I agree with your conclusions about add and use keyword. Also put gives false intuition with mutations, but in this case I would treat it as "put this to the object which is under construction right now" not "put something to object already created", so, I would still consider it as good option. include sounds very good, so probably it is better than inherit (although same size). Also insert could be considered, one letter less, but again it gives intuition related to mutations.

So I think include is a winner.

Is OOP still as pervasive as it was in the ’90s & ’00s?

@toastal yep, sure :)