Skip to content

Nested tuple unpacking #194

@metagn

Description

@metagn

This issue now proposes nested tuple unpacking in the core language, I have implemented some of the features I previously proposed in a small library: https://github.com/hlaaftana/definesugar.

Relevant issues on the Nim repo: nim-lang/Nim#13439, nim-lang/Nim#2436

The current tuple unpacking syntax is limited to:

let (a, b*, c) = (1, 2, 3)

This issue proposes:

let (a, (b*, c)) = (1, (2, 3))

Main use is for sugar/optimization in for loops:

for i, (a, b) in {"a": 1, "b": 2}:
  discard

Don't know if it's useful in any other context.

The old version of the RFC (long, only keeping as reference):

2. Multiple definitions of the same term

I thought of this syntax:

let (a, (b, c) as y) as x = (1, (2, 3))
var (a, b) as (c, d) = (1, 2)

but this looks kind of ugly and in vars, this would copy instead of alias as the word as implies.

var (a, b) as x = (1, 2)
x[0] = 3
echo a # 1

The solution is just the way you do nested tuples now:

let
  x = (1, (2, 3))
  (a, y) = x
  (b, c) = y
let irrelevant = (1, 2)
var
  (a, b) = irrelevant
  (c, d) = irrelevant

3. Unpacking in routine arguments

I realized after the fact that the macro solution for this is more expressive than the syntax solution.

proc foo((a, b): (int, int)) =
  discard

let x = (1, 2)
foo(x)
var t: Thread[(int, int)]
t.createThread(foo)

vs:

proc foo(x, y: int, str: string) {.packArgs.} =
  discard

var t: Thread[firstArgType(foo)]
t.createThread(foo)

proc foo(a: bool, x, y: int, str: string) {.packArgs(1..2).} =
  discard
foo(true, (1, 2), "str")

4. Named tuple unpacking

By typing out the fields, you can make unpacking named tuples field-order-agnostic.

type Person = tuple[name: string, age: int]
let (age: a, name: n) = Person("John Smith", 30)

Note that this depends on any var (a: int, b: int) = (1, 2) syntax not existing, as it doesn't now.

This could also be extended to objects/ref objects/anything with fields, but it would have to be duck typed. For objects, it would mean the left hand side of an assignment could have the side effect of throwing a FieldError, because of case object fields. This would be solved if the ProveField warning was turned into an error in the specific case of a case object field being unpacked, but still would have no solution for a general field access unpacking.

type Obj = object
  field1: int
  case isBranch: bool
  of true:
    field2: int
  else: discard
let (field1: a, field2: b) = Obj(field1: 1, isBranch: false) # compile time error

5. Splat/spread unpacking

I wrote this without postfix * in mind (in a macro it would be replaced with some other prefix) and I don't think it makes that much sense for tuples. Keeping it here for reference

Nim has no concept of a splat/spread operator as in other languages, so this would be a completely new idea to Nim. My suggestion in this case is a prefix * as in Python. The key use of this is only getting the first/last few elements of a tuple, using the _ special symbol, although most tuples are too small for this to matter.

let (a, b, *c, d) = (1, 2, 3, 4, 5)
let (a, b, *_) = (1, 2, 3, 4, 5)

There can only be 1 splat/spread in a single unpack. Splatting could also work for named tuples, but the compiler would have to create a new named tuple type, so it would not work for objects.

6. Array/generic subscript unpacking

This was originally simpler but it doesn't fit the core language so I mixed it with other wild ideas and am keeping it here for reference.

You can either extend the let (,) syntax to work for anything you can index from (not the best idea), or introduce a new let [,] syntax for arrays, which is excessive because it implies you can do [a, b] = [1, 2] as a normal assignment.

var arr: array[1..10, someType]
var [firstElem, *rest] = arr
var i: someType
[firstElem, i] = [1, 2]

Having a distinct syntax for arrays than for tuples would mean you could index by enums just like you can do in normal arrays.

type StreetlightColor = enum
  Red, Yellow, Green

var areOn: array[StreetlightColor, bool]
let [Red: redOn, Yellow: yellowOn, Green: greenOn] = areOn

Combined with splatting though, this syntax can get complex.

type Days = enum
  Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday

var arr: array[Days, bool]
let [Monday..Thursday: *weekdayArr, Friday: fridayValue, Saturday..Sunday: *weekendArr] = arr
let [Monday: *weekdayArr[4], Friday: fridayValue, Saturday: *weekendArr[2]] = arr

This could be extended for anything that you can index like seqs or strings, but it would have the same side effect problem as 4.

let [0: first, 0..^1: all, ^3: lastThird] = @[1, 2, 3, 4, 5]

So you could have an alternative syntax that implies side effects are possible like so:

let {0: first, 0..^1: all, ^3: lastThird} = @[1, 2, 3, 4, 5]
import json
let {"a": a, "b": b} = %*{"a": 1, "b": 2}
# expands to
let table = %*{"a": 1, "b": 2}
let a = table["a"]
let b = table["b"]

This is also weird syntax because {} constructs arrays of 2-tuples and sets and works as its own subscript operator, it has nothing to do with [] subscripting.

Custom unpacking macros:

This would be an experimental feature akin to case statement macros, term rewriting macros, for statement macros etc. that requires a parser change. I don't know how the parser works exactly, but the way I see it it would go like this: let a = b and var and const can have any untyped expression for a that a = b would allow, if it's not one of the predefined assignment syntaxes from Nim. For example, if Nim is parsing (a, b, (c, d)), and reaches (a, b, then gets the token (, it rerecords the tokens starting from the first ( as an expression, then it's mapped to an unpack(lhs: untyped, rhs: T) macro. This returns an nnkAsgn that gets remapped inside a let/var/const statement or stays as nnkAsgn if it's from an assignment. The fact that let (a, b) and let a are parsed normally and not as expressions means those would not be handled by unpack, meaning you can return them as the concrete value of unpack.

import options, macros
let opt = some 3
macro unpack[T](lhs: untyped, rhs: Option[T]): untyped =
  template invalid = error("invalid left hand side for Option", lhs)
  case lhs.kind
  of nnkCall:
    if lhs.len == 2 and lhs[0] == ident"Some":
      result = newAssignment(lhs[1], newCall(bindSym"get", rhs))
    else: invalid
  of nnkIdent, nnkAccQuoted:
    # this should be unreachable, `let ident = expr` is always handled by Nim
    warning("got identifier/accent for left hand side of unpack, should not happen", lhs)
    result = newAssignment(lhs, rhs)
  else:
    invalid
let Some(val) = opt
echo val # 3

The only difference here between this and case statement macros is that case statement macros do not recheck for another case statement macro, but the unpack macro would. Ie:

import macros

{.experimental: "caseStmtMacros".}

type X = distinct string

macro match(s: X): untyped =
  result = quote do:
    echo "recursion worked"

macro match(n: tuple): untyped =
  result = newTree(nnkCaseStmt)
  result.add(newCall(bindSym"X", n[0][0]))
  for i in 1 ..< n.len:
    let it = n[i]
    case it.kind
    of nnkElse, nnkElifBranch, nnkElifExpr, nnkElseExpr:
      result.add it
    of nnkOfBranch:
      let br = newTree(nnkOfBranch)
      let min1 = it.len - 1
      for j in 0..<min1:
        br.add(newCall(bindSym"X", it[j][0]))
      br.add(it[min1])
      result.add(br)
    else:
      error "'match' cannot handle this node", it

case ("foo", 78)
of ("foo", 78): echo "tuple yes"
of ("bar", 88): echo "tuple no"
else: discard

prints "tuple yes". The problem with recursing is that it may go on infinitely, which is solved by just doing echo result.repr, maybe a explain macro like what concepts have or a hard limit like term rewriting macros have.

Here is a JSON concept:

import json

let obj = parseJson"""{"name": "John Smith", "age": 30, "children": ["James", "Mary"]}"""
let Object{name: String(name), age: Int(age), children: Array[String](children)} = obj

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions