Closure Operators Migration Guide
This guide shows how to migrate from Nextflow’s index-based groupTuple, join, and
combine operators to the closure-based groupTupleBy, joinBy, and combineBy
operators provided by nf-bids.
Quick reference
| Index-based | Closure-based | Key benefit |
|---|---|---|
|
|
Semantic grouping; works with maps |
|
|
No tuple restructuring needed |
|
|
Self-documenting, semantic fields |
Installation
nextflow.configplugins { id 'nf-bids@0.3.0' }
Workflow script
include { groupTupleBy } from 'plugin/nf-bids'
include { joinBy } from 'plugin/nf-bids'
include { combineBy } from 'plugin/nf-bids'
groupTuple → groupTupleBy
Single-field grouping
// Before: requires tuple, opaque index
channel.of(['sub-01', 'file1.nii'], ['sub-01', 'file2.nii'])
.groupTuple(by: 0)
// → ['sub-01', ['file1.nii', 'file2.nii']]
// After: map, semantic field
channel.of([subject: 'sub-01', file: 'file1.nii'],
[subject: 'sub-01', file: 'file2.nii'])
.groupTupleBy { it.subject }
// → ['sub-01', [[subject:'sub-01', file:'file1.nii'],
// [subject:'sub-01', file:'file2.nii']]]
groupTupleBy keeps the full item in the list; groupTuple decomposes it.
Downstream code must use items.collect { it.file } instead of files[0].
|
Composite key
// Before
channel.map { [it.subject + '_' + it.session, it] }
.groupTuple(by: 0)
// After
channel.groupTupleBy { [it.subject, it.session] }
// Closure returns List → wrapped in CompositeKey for correct equality
join → joinBy
Simple join
// Before: must restructure both channels first
left = left_ch.map { [it.id, it] }
right = right_ch.map { [it.id, it] }
left.join(right).view { key, l, r -> ... }
// After: no restructuring
left_ch.joinBy(right_ch) { it.id }
.view { fused -> ... }
joinBy now emits fused items (without the key). Keep extraction logic in the
closure and consume a single fused item downstream.
|
combine → combineBy
Common pitfalls
joinBy output is a fused item
// ❌ WRONG — expecting three tuple values
left.joinBy(right) { it.id }.view { key, l, r -> println l }
// ✅ CORRECT
left.joinBy(right) { it.id }.view { fused -> println fused }
Items are preserved as maps
// groupTuple decomposes the tuple:
channel.of(['A', 1, 'x']).groupTuple(by: 0)
// → ['A', [1], ['x']] ← separate lists per element
// groupTupleBy preserves items:
channel.of([group: 'A', val: 1]).groupTupleBy { it.group }
// → ['A', [[group:'A', val:1]]] ← items as-is in a single list
// Access: items.collect { it.val }
When to stay on index-based operators
Closure-based operators add a small overhead (~10–30 ms for typical BIDS datasets). Prefer the built-in operators when:
-
Items are simple fixed-position tuples with no semantic fields.
-
Maximum throughput is required and the dataset exceeds 100 k items.
-
Integrating with existing code that already produces the right tuple shape.