Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Destructuring #81

Closed
yannham opened this issue Jun 1, 2020 · 6 comments
Closed

Destructuring #81

yannham opened this issue Jun 1, 2020 · 6 comments
Assignees

Comments

@yannham
Copy link
Member

yannham commented Jun 1, 2020

Records and arrays are the two ubiquitous and fundamental data structures of a configuration language, be it Nix expressions, Jsonnet, Cue, or Nickel. It is useful in practice to be able to easily extract a few specific fields from a record (which may be located deep inside), or a few elements at specific positions from a list. To this end, we propose to add destructuring to Nickel, a syntax that allows to directly extract sub-elements from records or lists by extending let-bindings to bind not only variables, but patterns.

There are good examples out there to take inspiration from, such as pattern matching (restricted to records and lists) in Haskell or OCaml, or the closest to the following proposal, destructuring in Javascript.

Nix expressions also offer destructuring facilities:

  • Argument destructuring, which can only be done on the argument of a function
  • let inherit, which can be confused with its twin { inherit ...; }, and cannot specify default values for missing fields

Both cannot handle nested records nor bind a field to a variable with a different name. Lists cannot be destructured either. We intend to overcome these limitations in Nickel.

In the following, we provide a concrete proposal, as a starting point for discussion.

Motivating examples

We would like to write this kind of expressions:

let {pname; src=srcSome; meta={description};} = somePkg in foo
/* bind pname to somePkg.pname,
 * srcSome to somePkg.src,
 * and description to somePkg.meta.description in foo */

let {foo ? "default"; bar;} = {bar=1;} in baz
/* bind foo to "default" and bar to 1 in baz */

let [fst, snd, {bar}] = [1, 2, {bar="bar";}, "rest", "is", "ignored"] in foo
/* bind fst to 1, snd to 2 and bar to "bar" in foo */

let [head, ...tail] = [1, 2, 3, 4] in foo
/* bind head to 1 and tail to [2, 3, 4] in foo */

let [_, _, third, fourth] = [1, 2, 3, 4] in foo
/* bind third to 3 and fourth to 4 in foo */

let {a; ...rest} = {a=1; b=2; c=3;} in foo
/* bind a to 1 and rest to {c=2; d=3;} in foo */

let f = fun {x; y; z;} => x + y + z in foo
/* bind x, y and z to the corresponding fields of the argument in the body of f */

Description

We propose JavaScript-like destructuring, with the @ from Nix for capturing the whole match, featuring:

  • Nested patterns: we can destructure deeply nested records and arrays in a
    single pattern
  • Rebinding: we can bind the field of a record to a variable with a different name
  • Rest capture: we can capture the rest of the structure being matched using the syntax ...foo
  • Full match capture: we can capture the whole expression being matched in a variable with the syntax @foo
  • Default value: we can provide a default value for when a record field or an array element is missing
  • Ignore: use a _ pattern in a list to ignore an element

Syntax

let-binding ::= let letpat = e in e
fun-pat ::= fun letpat1 ... letpatn => e

let-pat ::= pat | pat @ var
pat ::= var | { recpat1; ...; recpatn; rest } | [lstpat1, ..., lstpatn, rest]
rest ::= ε | ...var

recpat ::= fldpat | fldpat ? e
fldpat ::= field | field = pat
lstpat ::= pat | pat ? e | _

Here e is an arbitrary Nickel expression, field is a field identifier, var is a variable identifier, and ε means "empty" or "absent".

Semantics

Source expression Rewrites to Condition
let pat@var = e in e' let var = e in let pat = var in e'
let { field; Q } = e in e' let { field = field; Q } = e in e'
let { field ? default; Q } = e in e' let { field = field ? default; Q } = e in e'
let { field = pat; Q } = e in e' let pat = e.field in let { Q } = e -$ "field" in e'
let { field = pat ? default; Q } = e in e' let pat = if hasField e field then e.field else default
in let { Q } = e -$ "field" in e'
let { ...rest} = e in e' let rest = e in e' isRec e
let { } = e in e' e' isRec e
let [pat, Q] = l in e' let pat = head l in let [Q] = tail l in e'
let [pat ? e, Q] = l in e' let pat = if isEmpty l then e else head l
in let [Q] = tail l in e'
let [_, Q] = l in e' let [Q] = tail l in e'
let [...rest] = l in e' let rest = l in e' isList l
let [ ] = l in e' l isList l
fun pat1 ... patn => e fun x1 ... xn => let pat1 = x1 in ... let patn = xn in e x1, ..., xn fresh

Q denotes the queue of a sequence of declarations, and can be empty. The -$ operator removes a field from a record.

Remarks

We proposed a rich set of features, for completeness and consistency. Even if they do not cost much to add, some are of questionable value, and could be considered for removal or restriction. Notably:

  • The ...rest syntax is handy for popping a few values from a list and get back the queue in one match, but what it adds for records patterns is not clear
  • Default values are handy inside records pattern, in particular when destructuring function arguments, but seem less valuable for lists.
@yannham yannham changed the title Add destructuring Destructuring Jun 2, 2020
@thufschmitt
Copy link
Member

It looks like contrary to Nix there's no exhaustiveness check in the pattern-matching of records (in the sense that let { x } = { x = 1; y = 2; } in x is a valid expression). Is that an explicit design choice or an overlook?

@yannham
Copy link
Member Author

yannham commented Jun 3, 2020

It looks like contrary to Nix there's no exhaustiveness check in the pattern-matching of records (in the sense that let { x } = { x = 1; y = 2; } in x is a valid expression). Is that an explicit design choice or an overlook?

Indeed. This is deliberate, as in the JavaScript syntax. I guess the question is: is destructuring currently used in Nix and intended to be used in Nickel as just a convenient syntax for extracting some fields from a (probably bigger) record, or is it rather a form of pattern matching and actually used for checking the structure of the matched record. In the former case, the exhaustiveness check is probably an annoyance at best. In the latter, an exhaustiveness check is indeed desirable, and partial matching could probably have the same syntax as in Nix: {foo=bar; ...}, which is also consistent with the syntax ...rest.

@aspiwack
Copy link
Member

aspiwack commented Jun 3, 2020

I'm in favour of {foo; bar} having a closed semantics, and have a semantics for open matching. We really want both, I think.

@edolstra
Copy link
Contributor

I'm not convinced a language that's supposed to be minimalistic should have this (at least not in version 1.0). Even the minimal destructuring supported by Nix isn't really necessary.

@yannham yannham mentioned this issue Jun 26, 2020
@Profpatsch
Copy link

Profpatsch commented Jul 1, 2020 via email

@yannham
Copy link
Member Author

yannham commented Jan 13, 2022

Closed by #567 and #473.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants