Difference between named tuples and records

What’s the difference between named tuples and records? Using tuples seems to be the standard approach, so that’s what I’ve mostly used so far. However, I encountered this issue today, which I reproduced in this simplified workflow:

nextflow.enable.dsl = 2
nextflow.enable.types = true

process process1_tuple {
  input:
  s: String

  script:
  '''
  touch file.json
  '''

  output:
  tuple(
    s: s,
    f: file('file.json')
  )
}
process process2_tuple {
  input:
  tuple(
    s: String,
    f: Path,
  )

  script:
  """
  ls -l ${f}
  """
}

workflow {
  ch = process1_tuple('hello')
  process2_tuple(ch)
}

producing the error

 N E X T F L O W   ~  version 26.04.0

Launching `main.nf` [special_rubens] revision: afb6ad2ce4

WARN: Static typing is a preview feature -- syntax and behavior may change in future releases
executor >  local (1)
[5b/fe4274] process1_tuple | 1 of 1 ✔
[-        ] process2_tuple -
[[s:hello, f:/home/valter/Downloads/nf-thing/work/5b/fe4274c04fbbc5f20d4dbe90662bd9/file.json]]
ERROR ~ Error executing process > 'process2_tuple'

Caused by:
  [process2_tuple] input at index 0 expected a tuple with 2 elements but received 1 -- offending value: [[s:hello, f:/home/valter/Downloads/nf-thing/work/5b/fe4274c04fbbc5f20d4dbe90662bd9/file.json]]

I don’t really understand what I’m doing wrong here. Since the output of process1_tuple looks to be the same as the input of process2_tuple, shouldn’t I just be able to pass it forward? Interestingly, the problem disappears if I just replace the named tuples with records instead:

nextflow.enable.dsl = 2
nextflow.enable.types = true

process process1_record {
  input:
  s: String

  script:
  '''
  touch file.json
  '''

  output:
  record(
    s: s,
    f: file('file.json')
  )
}
process process2_record {
  input:
  record(
    s: String,
    f: Path,
  )

  script:
  """
  ls -l ${f}
  """
}

workflow {
  ch = process1_record('hello')
  process2_record(ch)
}

Since I like to have most things named and typed, should I always prefer records over named tuples?

Named tuples aren’t really a thing in Nextflow. Although I can see why you might think that with the tuple input syntax:

tuple(s: String, f: Path)

But if you create a tuple with named arguments, it will actually create a tuple with one map element:

tuple(s: s, f: file('file.json'))

// equivalent
tuple([s: s, f: file('file.json'])

That’s just because of how named arguments are de-sugared in Nextflow. Maybe the type checker should warn about this potential foot-gun…

In any case, if you are comfortable using records then I would just use records. The tuple syntax is useful for converting your processes to static typing without having to change any workflow logic. Records are nicer but they require a lot of refactoring in your workflow logic. So if you don’t want to do that yet, just use the tuple syntax for now, and then switching processes from tuples to records is a simple search and replace tuple(record(.

I’m quite new to Nextflow actually and prefer static typing, so I’ll just stick to records then. Thanks!