Skip to content

Commit 35a0788

Browse files
committed
Support process named arguments in typed workflows
Signed-off-by: Ben Sherman <bentshermann@gmail.com>
1 parent 090d9f2 commit 35a0788

4 files changed

Lines changed: 144 additions & 2 deletions

File tree

docs/workflow-typed.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,70 @@ The `Value` type supports the following operators:
181181

182182
See {ref}`operator-typed-page` for more information about each operator. See {ref}`migrating-typed-operators` to learn how to migrate existing operators to typed workflows.
183183

184+
### Process named arguments
185+
186+
A common pattern is for a workflow to combine a channel with one or more dataflow values into a single input for a process:
187+
188+
```nextflow
189+
nextflow.preview.types = true
190+
191+
process align {
192+
input:
193+
input: Record {
194+
id: String
195+
fastq: Path
196+
index: Path
197+
}
198+
199+
// ...
200+
}
201+
202+
workflow align_dedup {
203+
take:
204+
ch_samples: Channel<Sample>
205+
index: Value<Path>
206+
207+
main:
208+
ch_align_inputs = ch_samples
209+
.cross(index)
210+
.map { sample, index -> sample + record(index: index) }
211+
212+
align( ch_align_inputs )
213+
214+
// ...
215+
}
216+
217+
record Sample {
218+
id: String
219+
fastq: Path
220+
}
221+
```
222+
223+
This pattern requires a `cross` and `map` operation for each dataflow value that must be added, which quickly becomes verbose.
224+
225+
In a typed workflow, the same behavior can be achieved by calling the process with named arguments:
226+
227+
```nextflow
228+
workflow align_dedup {
229+
take:
230+
ch_samples: Channel<Sample>
231+
index: Value<Path>
232+
233+
main:
234+
align(ch_samples, index: index)
235+
236+
// ...
237+
}
238+
```
239+
240+
The named arguments supplied to `align` are automatically added to each record in `ch_samples`, producing the equivalent of `ch_align_inputs` in the previous example.
241+
242+
Named arguments can be used with a process under the following conditions:
243+
244+
- The process declares a single record input
245+
- The positional argument (i.e. `ch_samples`) is a channel of records
246+
- The named arguments are regular values or dataflow values, not channels
247+
184248
### Restricted syntax
185249

186250
The following syntax patterns are not supported in typed workflows.

modules/nextflow/src/main/groovy/nextflow/script/ProcessDef.groovy

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ import nextflow.script.dsl.ProcessConfigBuilder
3333
import nextflow.script.params.BaseInParam
3434
import nextflow.script.params.BaseOutParam
3535
import nextflow.script.params.EachInParam
36+
import nextflow.script.params.v2.ProcessInputsDef
37+
import nextflow.script.types.Record
38+
import nextflow.util.RecordMap
3639

3740
/**
3841
* Models a nextflow process definition
@@ -209,6 +212,9 @@ class ProcessDef extends BindableDef implements IterableDef, ChainableDef {
209212
final declaredInputs = config.getInputs()
210213
final declaredOutputs = config.getOutputs()
211214

215+
// combine named args if applicable
216+
args = combineNamedArgs(args, declaredInputs)
217+
212218
// validate arguments
213219
if( args.size() != declaredInputs.size() )
214220
throw new ScriptRuntimeException(missMatchErrMessage(processName, declaredInputs.size(), args.size()))
@@ -243,6 +249,23 @@ class ProcessDef extends BindableDef implements IterableDef, ChainableDef {
243249
null
244250
}
245251

252+
private Object[] combineNamedArgs(Object[] args, ProcessInputsDef declaredInputs) {
253+
if( !(args.length == 2 && args[0] instanceof Map) )
254+
return args
255+
if( !(declaredInputs.size() == 1 && Record.class.isAssignableFrom(declaredInputs[0].getType())) )
256+
return args
257+
final opts = (Map<String,Object>) args[0]
258+
def result = args[1]
259+
for( final name : opts.keySet() ) {
260+
final value = opts[name]
261+
if( result instanceof ChannelImpl )
262+
result = result.cross(value).map { List rv -> ((RecordMap) rv[0]).plus(new RecordMap([(name): rv[1]])) }
263+
else if( result instanceof ValueImpl )
264+
result = result.cross(value).map { List rv -> ((RecordMap) rv[0]).plus(new RecordMap([(name): rv[1]])) }
265+
}
266+
return new Object[] { result }
267+
}
268+
246269
private DataflowReadChannel createSourceChannel(Object value) {
247270
if( value instanceof ChannelImpl )
248271
return CH.getReadChannel(value.getSource())

modules/nf-lang/src/main/java/nextflow/script/control/TypeCheckingVisitor.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,18 @@
1717

1818
import nextflow.script.ast.ASTNodeMarker;
1919
import nextflow.script.ast.ProcessNode;
20+
import nextflow.script.ast.ProcessNodeV2;
21+
import nextflow.script.ast.RecordNode;
2022
import nextflow.script.ast.ScriptNode;
2123
import nextflow.script.ast.ScriptVisitorSupport;
2224
import nextflow.script.ast.WorkflowNode;
25+
import nextflow.script.types.Record;
2326
import org.codehaus.groovy.ast.ASTNode;
27+
import org.codehaus.groovy.ast.ClassHelper;
28+
import org.codehaus.groovy.ast.ClassNode;
2429
import org.codehaus.groovy.ast.MethodNode;
2530
import org.codehaus.groovy.ast.expr.MethodCallExpression;
31+
import org.codehaus.groovy.ast.expr.NamedArgumentListExpression;
2632
import org.codehaus.groovy.control.SourceUnit;
2733
import org.codehaus.groovy.control.messages.SyntaxErrorMessage;
2834
import org.codehaus.groovy.syntax.SyntaxException;
@@ -36,6 +42,8 @@
3642
*/
3743
public class TypeCheckingVisitor extends ScriptVisitorSupport {
3844

45+
private static final ClassNode RECORD_TYPE = ClassHelper.makeCached(Record.class);
46+
3947
private SourceUnit sourceUnit;
4048

4149
public TypeCheckingVisitor(SourceUnit sourceUnit) {
@@ -66,8 +74,25 @@ public void visitMethodCallExpression(MethodCallExpression node) {
6674
private void checkMethodCallArguments(MethodCallExpression node, MethodNode defNode) {
6775
var argsCount = asMethodCallArguments(node).size();
6876
var paramsCount = defNode.getParameters().length;
69-
if( argsCount != paramsCount )
70-
addError(String.format("Incorrect number of call arguments, expected %d but received %d", paramsCount, argsCount), node);
77+
if( argsCount == paramsCount )
78+
return;
79+
if( isProcessCallWithNamedArgs(node) )
80+
return;
81+
addError(String.format("Incorrect number of call arguments, expected %d but received %d", paramsCount, argsCount), node);
82+
}
83+
84+
private static boolean isProcessCallWithNamedArgs(MethodCallExpression node) {
85+
var defNode = (MethodNode) node.getNodeMetaData(ASTNodeMarker.METHOD_TARGET);
86+
var args = asMethodCallArguments(node);
87+
return defNode instanceof ProcessNodeV2
88+
&& defNode.getParameters().length == 1
89+
&& isRecordType(defNode.getParameters()[0].getType())
90+
&& args.size() == 2
91+
&& args.get(0) instanceof NamedArgumentListExpression;
92+
}
93+
94+
private static boolean isRecordType(ClassNode type) {
95+
return RECORD_TYPE.equals(type) || type.redirect() instanceof RecordNode;
7196
}
7297

7398
@Override

modules/nf-lang/src/test/groovy/nextflow/script/control/TypeCheckingTest.groovy

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,34 @@ class TypeCheckingTest extends Specification {
8585
errors[0].getOriginalMessage() == 'Incorrect number of call arguments, expected 2 but received 1'
8686
}
8787

88+
def 'should allow typed process to be called with named args' () {
89+
when:
90+
def errors = check(
91+
'''\
92+
nextflow.preview.types = true
93+
94+
process align {
95+
input:
96+
in: Record {
97+
id: String
98+
fastq: Path
99+
index: Path
100+
}
101+
102+
script:
103+
"""
104+
"""
105+
}
106+
107+
workflow {
108+
ch_samples = channel.of( record(id: '1', fastq: file('1.fastq')) )
109+
index = file('index.fasta')
110+
align( ch_samples, index: index )
111+
}
112+
'''
113+
)
114+
then:
115+
errors.size() == 0
116+
}
117+
88118
}

0 commit comments

Comments
 (0)