Semantics
The semantics of the Nextflow language are the meaning and behavior of each language construct. This page is a companion to the Syntax reference, which describes the grammar of the language (how constructs are written). See the Scripts page for a practical introduction to the language.
Values
Every expression in Nextflow produces a value, and every value has a type that determines the operations it supports. This section describes the kinds of values in the language and how they behave. See the Types reference for the full set of operations available on each type, and Syntax for the grammar of each literal. A boolean is either Any value can be coerced to a boolean based on its truthiness. A value that coerces to The truthiness of a value depends on its type: For example: A number is either an integer or a floating-point number. Numbers support the standard arithmetic operators. Arithmetic between two integers produces an integer, with one exception: division always produces a floating-point result. Use the When an operation mixes an integer and a floating-point operand, the integer is coerced to floating-point. A string is a sequence of characters, written with single or double quotes (see String for the grammar). Strings are immutable, and can be concatenated with the A double-quoted string supports interpolation -- embedded expressions are evaluated and their results inserted into the string. A single-quoted string is a literal and is never interpolated: An interpolated expression is written as Each interpolated expression is evaluated when the string is created, and its result is converted to a string using the value's Single-quoted strings are not interpolated: See String for the available operations. The special value A collection is a group of values. Nextflow provides three kinds of collection, which differ in whether they preserve the order of their elements and whether they allow duplicates: A list is the most commonly used collection. It is written with square brackets, and its elements are accessed by index, starting at zero. Lists are mutable -- elements can be added, removed, or replaced: A set is an unordered collection that cannot contain duplicates. It is typically created from a list with the A bag is an unordered collection that allows duplicates. It is used where the order of elements is not significant. All collections can be iterated with higher-order functions and tested with the membership operators. The See List<E>, Set<E>, and Bag<E> for the available operations. A map is a collection of key-value pairs, written with square brackets: Values are accessed by key, using either the index operator or a property expression: The See Map<K,V> for the available operations. A record holds a set of named fields, where each field can have its own type. Records are created with the Fields are accessed by name: Records are immutable -- once a record is created, it cannot be modified. Combine two records using the See Record for the available operations. A tuple is a fixed sequence of values, which may have different types. Tuples are created with the Tuple elements are accessed by index: Tuples are immutable -- once a tuple is created, its elements cannot be modified. A tuple can be destructured into multiple variables, both in assignments and in closure parameters. See Tuple for the available operations. A closure is an anonymous function that is itself a value. A closure consists of a parameter list and a body, enclosed in curly braces: Closures have the same parameter and return semantics as function. A closure can access variables from the enclosing scope, and can declare local variables that exist only for the duration of each invocation: When a closure receives a single tuple argument, the parameter list can destructure the tuple. The number of parameters must match the number of tuple elements: Within a nested closure, The primary use of a closure is as an argument to a higher-order function -- a function that takes a closure and applies it, such as the iteration methods of a collection or the operators of a channel.Booleans
true or false. Booleans are produced by the relational and logical operators and consumed by constructs that test a condition, such as if/else and assertions.true is truthy, and a value that coerces to false is falsy.Type Falsy values Truthy values Boolean falsetrueNumber zero ( 0, 0.0)any non-zero number String empty string ( '')any non-empty string Collection empty collection ( [])any non-empty collection Map empty map ( [:])any non-empty map Everything else nullany non-null value assert true
assert 42
assert 'hello'
assert [1, 2, 3]
assert [key: 'value']
assert !false
assert !0
assert !''
assert ![]
assert ![:]
assert !nullNumbers
intdiv method for integer division.assert 2 + 3 == 5
assert 7 / 2 == 3.5 // division produces a float
assert 7.intdiv(2) == 3 // integer divisionStrings
+ operator:def greeting = 'Hello, ' + 'world!'def name = 'world'
println "Hello, ${name}!" // -> Hello, world!
println 'Hello, ${name}!' // -> Hello, ${name}!${expression}. The curly braces can be omitted for a simple variable or property reference:def sample = [id: 'A', count: 42]
println "Sample $sample.id has $sample.count reads"
// -> Sample A has 42 readstoString() method. Interpolation is eager -- the expression is evaluated immediately, not deferred:def x = 1
def s = "x is ${x}"
x = 2
println s // -> x is 1println 'Hello, ${name}!'
// -> Hello, ${name}!Null
null represents the absence of a value. Accessing a property or calling a method on null raises a null reference error. Avoid null where possible. The safe navigation operator can be used to access a property that may be null.Collections
Collection Ordered Duplicates List yes yes Set no no Bag no yes def numbers = [1, 2, 3]
assert numbers[0] == 1toSet() method:def unique = [1, 2, 2, 3].toSet()
assert unique.size() == 3+ operator returns a new collection containing the elements of both operands, rather than modifying either one:assert [1, 2] + [3, 4] == [1, 2, 3, 4]Maps
def scores = [alice: 100, bob: 92]assert scores['alice'] == 100
assert scores.alice == 100+ operator returns a new map containing the entries of both operands, with the right operand taking precedence on conflicting keys:assert [a: 1, b: 2] + [b: 3] == [a: 1, b: 3]Records
record() function:def person = record(name: 'Alice', age: 42)assert person.name == 'Alice'+ operator to produce a new record:def older = person + record(age: 43)
// -> record(name: 'Alice', age: 43)Tuples
tuple() function:def pair = tuple('sample_1', file('sample_1.fastq'))assert pair[0] == 'sample_1'Closures
def square = { v -> v * v }
assert square.call(9) == 81def factor = 2
assert [1, 2, 3].collect { v -> factor * v } == [2, 4, 6]def samples = [
tuple('sample_1', 100),
tuple('sample_2', 200),
]
samples.each { id, count ->
println "${id}: ${count}"
}return exits the nearest enclosing closure, not the outer function.
Operators
An operator is a symbol that performs an operation on one or two operands. The full set of operators and their grammar is listed in the Syntax reference. This section describes their semantics, grouped by category.
Arithmetic operators
Arithmetic operators perform numeric computation:
| Operator | Operation | Example |
|---|---|---|
+ | addition | 2 + 3 -> 5 |
- | subtraction | 5 - 2 -> 3 |
* | multiplication | 2 * 3 -> 6 |
/ | division | 7 / 2 -> 3.5 |
% | remainder (modulo) | 7 % 3 -> 1 |
** | power (exponentiation) | 2 ** 8 -> 256 |
Division of two integers produces a floating-point result (7 / 2 is 3.5, not 3). To perform integer division, use the intdiv method: 7.intdiv(2) is 3.
Some arithmetic operators are overloaded for non-numeric types. In particular, + concatenates strings, lists, and maps:
assert 'foo' + 'bar' == 'foobar'
assert [1, 2] + [3, 4] == [1, 2, 3, 4]
assert [a: 1] + [b: 2] == [a: 1, b: 2]
Relational operators
Relational operators compare two values and produce a boolean:
| Operator | Operation |
|---|---|
== | equal to |
!= | not equal to |
< | less than |
> | greater than |
<= | less than or equal to |
>= | greater than or equal to |
<=> | three-way comparison (spaceship) |
In Nextflow, == tests value equality, not object identity -- two distinct objects with the same contents are equal:
assert [1, 2, 3] == [1, 2, 3]
assert 'hello' == 'hello'
Ordering operators (<, >, <=, >=) compare values according to their natural order: numbers numerically and strings alphabetically. The spaceship operator <=> returns -1, 0, or 1 depending on whether the left operand is less than, equal to, or greater than the right operand, and is useful for defining custom sort comparators:
def items = ['banana', 'apple', 'cherry']
items.toSorted { a, b -> a <=> b }
// -> ['apple', 'banana', 'cherry']
Logical operators
Logical operators combine boolean values. Their operands are coerced based on truthiness:
| Operator | Operation |
|---|---|
&& | logical AND |
|| | logical OR |
! | logical NOT |
The && and || operators short-circuit: the right operand is evaluated only if necessary. For &&, the right operand is evaluated only if the left is truthy. For ||, it is evaluated only if the left is falsy. This allows the right operand to safely depend on the left:
if( reads != null && reads.size() > 0 )
// reads.size() is only evaluated if reads is not null
Bitwise operators
Bitwise operators operate on the individual bits of integer values:
| Operator | Operation |
|---|---|
& | bitwise AND |
| | bitwise OR |
^ | bitwise XOR |
~ | bitwise NOT |
<< | left shift |
>> | right shift |
>>> | unsigned right shift |
assert (0b1100 & 0b1010) == 0b1000 // -> 8
assert (0b1100 | 0b1010) == 0b1110 // -> 14
assert (1 << 4) == 16
The >> operator is also used in typed processes to emit values to a topic, and in workflow outputs to publish files.
Conditional operators
Conditional operators select a value based on a condition.
The ternary operator cond ? a : b evaluates cond (using truthiness) and returns a if it is truthy, otherwise b:
def label = sample.single_end ? 'single' : 'paired'
The Elvis operator a ?: b is a shorthand that returns a if it is truthy, otherwise b. It is commonly used to supply a default value:
def threads = params.threads ?: 1
In both cases, only the selected branch is evaluated.
Navigation operators
Navigation operators access properties and methods of a value.
The dot operator a.b accesses the property b (or calls the method b()) of a:
myFile.name
myFile.getName()
The safe dot operator a?.b returns null if a is null, instead of raising a null reference error. This avoids the need for explicit null checks:
def name = sample?.reads?.name
// null if sample or sample.reads is null
The spread dot operator a*.b applies the property access or method call to each element of a collection, returning a list of the results:
def files = [file('a.txt'), file('b.txt')]
files*.name
// -> ['a.txt', 'b.txt']
// equivalent to: files.collect { f -> f.name }
Range operators
Range operators create a list of consecutive values. A range can be formed from any ordered type, such as integers or characters:
| Operator | Operation |
|---|---|
.. | inclusive range |
..< | right-exclusive range |
assert (1..5) == [1, 2, 3, 4, 5]
assert (1..<5) == [1, 2, 3, 4]
assert ('a'..'f') == ['a', 'b', 'c', 'd', 'e', 'f']
Membership operators
Membership operators test whether a collection contains a value:
| Operator | Operation |
|---|---|
in | membership |
!in | negated membership |
assert 2 in [1, 2, 3]
assert 4 !in [1, 2, 3]
For a map, the in operator tests for the presence of a key.
Type operators
Type operators convert or test the type of a value:
| Operator | Operation |
|---|---|
as | type cast |
instanceof | type test |
!instanceof | negated type test |
The as operator casts a value to a different type (see Types). The instanceof operator tests whether a value is an instance of a given type:
assert ('42' as Integer) == 42
assert 'foo' instanceof String
assert 42 !instanceof String
Regex operators
Regex operators match a string against a regular expression:
| Operator | Operation |
|---|---|
=~ | regex find |
==~ | regex match |
The =~ operator is truthy if the pattern occurs anywhere in the string, whereas ==~ is true only if the entire string matches the pattern:
assert 'hello world' =~ /world/ // find: pattern occurs in the string
assert !('hello world' ==~ /world/) // match: pattern must match the whole string
assert '2.7.3' ==~ /\d+\.\d+\.\d+/
Operator precedence
When an expression contains multiple operators, precedence determines the order in which they are applied. Operators with higher precedence are applied before those with lower precedence. For example, * has higher precedence than +, so 1 + 2 * 3 is 7, not 9.
The full precedence order, from highest to lowest, is listed in the Syntax reference. The most important rules to remember are:
- Property access, method calls, and index expressions bind most tightly.
- Unary operators (
!,~, unary-) bind more tightly than binary arithmetic. - Arithmetic binds more tightly than comparison, which binds more tightly than the logical operators.
- The conditional operators (
?:) have the lowest precedence.
When in doubt, use parentheses to make the intended grouping explicit:
(1 + 2) * 3 // -> 9
1 + 2 * 3 // -> 7
Statements
A statement performs an action, such as declaring a variable, branching on a condition, or raising an error. Statements are executed in order, top to bottom. A variable is declared with the Multiple variables can be declared in a single statement by destructuring a tuple. The number of variables must match the number of elements: Each variable has a scope, which is the region of code in which it can be used: Variables declared in a function, along with the function's parameters, exist for the duration of each function call. The same applies to closures. Workflow inputs exist for the entire workflow body. Variables declared in the Process input variables exist for the entire process body. Variables declared in the process Variables declared in an if or else branch exist only within that branch: A variable cannot be declared with the same name as another variable in the same scope or an enclosing scope: An assignment updates the value of a target, which can be a variable, an index expression, or a property expression: Multiple targets can be assigned in a single statement by destructuring a tuple, in the same way as a declaration but without A compound assignment combines a binary operation with the assignment. For example, Because the underlying operators are overloaded, compound assignment also works for strings and collections: The following compound assignment operators are available, each corresponding to a binary operator: The elvis assignment An if/else statement conditionally executes one of two branches based on a boolean condition. The condition does not need to be a boolean value -- it is coerced to a boolean using truthiness: An if/else statement is a statement, not an expression -- it does not produce a value. To select between two values based on a condition, use a conditional expression instead: Nextflow does not support imperative loop statements such as For example, instead of a Use the Higher-order functions make the intent of the iteration explicit. Common operations include: A higher-order function returns a value rather than mutating external state, which makes it easier to reason about and avoids common errors. For example, See Iterable<E> for the full set of higher-order functions for collections such as lists and sets. To process the values in a channel, use operators such as An assert statement evaluates a boolean condition and raises an error if the condition is false. Like an if condition, the asserted expression is coerced to a boolean using truthiness: If the condition is false, an error is raised. The optional message after the colon is included in the error. If no message is given, Nextflow reports the values of the sub-expressions in the condition: Assertions are intended for verifying invariants -- conditions that should always hold if the code is correct. They are commonly used in tests and for sanity checks. To validate user inputs or report expected error conditions, use the When an error occurs at runtime, an exception is raised. An exception propagates up through the call stack until it is caught by an enclosing try/catch statement, or else it reaches the Nextflow runtime and terminates the pipeline. The recommended way to raise an error in pipeline code is the The lower-level A try/catch statement is appropriate when an error is expected and can be handled in pipeline logic -- for example, falling back to a default value when an optional file cannot be read: A catch clause matches an exception if the raised exception is an instance of the declared type. If no catch clause matches, the exception propagates to the next enclosing try/catch statement, or terminates the pipeline. Use try/catch only for errors you intend to recover from -- otherwise, let the error propagate so the failure is visible. The The The Variable declarations
def keyword and an initial value:def x = 42def (id, reads) = tuple('sample_1', file('sample_1.fastq'))
// id -> 'sample_1'
// reads -> file('sample_1.fastq')
main: section exist for the main, emit, and publish sections. Named outputs are not variable declarations and therefore have no scope.script, exec, and stub sections exist only within their respective section, with one exception -- variables declared without the def keyword also exist in the output section.if( true )
def x = 'hello'
println x // error: `x` is undefined
// solution: declare `x` outside of the if branch
def x
if( true )
x = 'hello'
println xdef clash(x) {
def x // error: `x` is already declared
if( true )
def x // error: `x` is already declared
}Assignments
x = 42
list[0] = 'first'
map.key = 'value'def:(id, reads) = tuple('sample_1', file('sample_1.fastq'))x += y is equivalent to x = x + y -- the target is read, combined with the source, and the result assigned back:def x = 1
x += 2
assert x == 3def items = [1, 2]
items += [3]
assert items == [1, 2, 3]+=, -=, *=, /=, %=, **=, &=, |=, ^=, <<=, >>=, >>>=.?= is a special case: x ?= y is equivalent to x = x ?: y, assigning y only if x is currently falsy. It is useful for setting a default value:def x = null
x ?= 42
assert x == 42if/else
def files = file('*.txt')
if( files ) {
println "Found ${files.size()} files"
}
else {
println 'No files found'
}def message = files ? "Found ${files.size()} files" : 'No files found'Iteration
for and while. Instead, iteration is expressed declaratively using higher-order functions -- methods that take a closure and apply it to each element of a collection.for loop:// Groovy (not supported in Nextflow)
def result = []
for( x in [1, 2, 3] ) {
result.add(x * x)
}collect method to transform each element:def result = [1, 2, 3].collect { x -> x * x }
// -> [1, 4, 9]
each: perform a side effect for each elementcollect: transform each element into a new value (i.e. map)find / findAll: select the first / all elements that satisfy a condition (i.e. filter)inject: combine all elements into a single value (i.e. reduce or fold)any / every: test whether any / all elements satisfy a conditiondef numbers = [1, 2, 3, 4, 5]
numbers.each { v -> println v } // print each number
numbers.collect { v -> v * 2 } // -> [2, 4, 6, 8, 10]
numbers.findAll { v -> v % 2 == 0 } // -> [2, 4]
numbers.inject(0) { acc, v -> acc + v } // -> 15
numbers.every { v -> v > 0 } // -> trueinject accumulates a result without an external accumulator variable:// instead of mutating an external variable
def total = 0
[1, 2, 3].each { v -> total += v }
// accumulate the result directly
def total = [1, 2, 3].inject(0) { acc, v -> acc + v }map, filter, and reduce instead.Assertions
assert params.input != null : 'The --input parameter is required'assert 2 + 2 == 5
// Assertion failed:
// assert 2 + 2 == 5
// | |
// 4 falseerror function instead, which produces a cleaner message without the assertion trace.Error handling
error function, which stops the pipeline with a clear error message:if( params.aligner !in ['bowtie2', 'bwamem'] )
error "Unsupported aligner: ${params.aligner}"throw statement can be used to raise a specific exception type, but error should be preferred for reporting pipeline errors, as it produces a cleaner message and does not require constructing an exception:// preferred
error 'Something failed!'
// equivalent, but more verbose
throw new Exception('Something failed!')def config
try {
config = file('custom.config').text
}
catch( e: IOException ) {
log.warn 'Could not read custom.config, using defaults'
config = ''
}error function and try/catch handle errors in pipeline code (functions, closures, the workflow body). They are distinct from two other mechanisms that operate at different levels:
errorStrategy directive determines what happens when a process task fails (e.g. a command exits with a non-zero status): the task can be retried, ignored, or terminate the pipeline. This is the appropriate mechanism for handling failures of the tools that a process executes.onError workflow handler runs when the pipeline as a whole fails, allowing for cleanup or notification.
Functions
A function is a named, reusable block of code that takes zero or more parameters and may return a value:
def sayHello(name: String) {
println "Hello, ${name}!"
}
def add(a: Integer, b: Integer) -> Integer {
a + b
}
Parameters
Arguments are passed by reference -- the function receives a reference to the same object, not a copy. Mutating a mutable argument (such as a list or map) inside a function affects the caller's object:
def addItem(list, item) {
list.add(item)
}
def items = [1, 2]
addItem(items, 3)
println items // -> [1, 2, 3]
To avoid this, create a copy before mutating, or use operations that return a new value (e.g. list + [item] instead of list.add(item)).
Reassigning a parameter, on the other hand, does not affect the caller. The parameter is a separate reference to the same object, so assigning a new value to it changes only the function's local reference:
def replace(list) {
list = [4, 5, 6] // reassigns the local reference only
}
def items = [1, 2, 3]
replace(items)
println items // -> [1, 2, 3]
Default parameter values
A function parameter can declare a default value, which is used when the caller does not supply an argument for that parameter:
def greet(name, greeting = 'Hello') {
"${greeting}, ${name}!"
}
greet('world') // -> Hello, world!
greet('world', 'Hi') // -> Hi, world!
Parameters with default values must come after parameters without defaults. A default value can be any expression, and is evaluated each time the function is called without that argument.
Named arguments
A function call may use named arguments -- arguments specified by name rather than position. Named arguments are collected into a map and passed as the first positional argument to the function:
file('hello.txt', checkIfExists: true)
is equivalent to:
file([checkIfExists: true], 'hello.txt')
Named arguments are commonly used by built-in functions and process directives to specify optional settings:
// process directive with named options
accelerator 4, type: 'nvidia-tesla-v100'
// channel factory with named options
channel.fromPath('*.txt', checkIfExists: true)
Because named arguments are collected into a single map, their order does not matter, and they can be freely mixed with positional arguments. A function that accepts named arguments must declare a Map as its first parameter.
Returns
A function returns the value of its last evaluated expression, or the value of an explicit return statement:
def add(a, b) {
a + b // implicit return
}
def addExplicit(a, b) {
return a + b
}
If a function has multiple return statements (including implicit returns), they must either all return a value or all return nothing. When a value is returned, every conditional branch must return a value:
def isEven1(n) {
if( n % 2 == 1 )
return // error: a return value is required here
return true
}
def isEven2(n) {
if( n % 2 == 0 )
return true
// error: a return value is required here
}
If the last statement of a function is not a return or expression statement, it is equivalent to a bare return with no value.
Processes
A process defines a task that runs in an isolated environment. A process is called like a function from a workflow, but instead of running immediately, each call adds a node to the dataflow graph. Nextflow creates a separate task for each input (or combination of inputs) and executes these tasks asynchronously as their inputs become available, potentially in parallel and on distributed compute.
See Processes for a full description of how to define and use processes.
Workflows
A Nextflow workflow describes a dataflow -- a graph of processes and operators connected by channels. See Processes and Workflows for a full description of how to define and compose them. A workflow body is not executed step by step like an ordinary function. Instead, it is evaluated once, up front, to construct the dataflow graph. The statements in a workflow body declare how processes, operators, and channels are wired together. The actual computation happens afterward, asynchronously, as data flows through the graph. In this sense, a workflow definition behaves like a compile-time macro: it runs once to build the dataflow graph, and the operations it describes (process tasks, operator logic) execute later as data becomes available. Consider: When the workflow body runs: Only after the entire graph is constructed does Nextflow begin executing it: the channel emits its values, the This has several consequences: Channel contents cannot be accessed directly. Because the workflow body runs before any data flows, you cannot inspect the values in a channel from within the workflow body. You must use an operator or process to act on each value: Operator closures run asynchronously. The closure passed to an operator such as The order of statements describes connections, not execution order. Two independent branches of a workflow are constructed in the order written, but their tasks may execute concurrently. This is the fundamental difference between the workflow definition (evaluated synchronously to build the graph) and the workflow execution (runs the graph asynchronously). See Dataflow for more information.Definition vs execution
workflow {
ch_input = channel.of(1, 2, 3)
ch_result = ch_input.map { v -> expensive(v) }
ch_result.view()
}
channel.of(1, 2, 3) creates a channel..map { ... } connects a map operator to that channel and returns a new channel. The closure is not called yet -- it is registered to run later, once per value..view() connects a view operator.map closure runs for each value, and view prints each result.
// does NOT work -- the channel has no values yet when this runs
def first = ch_input[0]
// correct -- the closure runs later, for each value
ch_input.map { v -> transform(v) }map or filter is invoked later, once per channel value, and possibly out of order. It should not depend on mutable state shared with other closures.
Scripts
A script is a single Nextflow source file. It consists of script declarations -- such as workflows, processes, functions, and types -- and may use declarations from other scripts. When a script is executed, its entry workflow -- the workflow with no name -- serves as the entrypoint. A script that contains only statements, with no declarations, is treated as the body of an implicit entry workflow. To be executable, a script must either be a code snippet or define an entry workflow. A script can use the definitions of another script through an include declaration: The include source should refer to either a local path (e.g. The following definitions can be included from a script: The entry workflow of an included script is ignored. This allows the same script to be executed directly or included by another script.Execution
Inclusion
include { hallo as sayHello } from './some/module'./module.nf), a registry module (e.g. nf-core/fastq), or a plugin (e.g. plugin/nf-hello).
Types
Every value in Nextflow has a type, which determines the set of operations that can be performed on it. The available types are described in the Types reference.
Nextflow supports two styles of typing:
-
Dynamic typing (the default): variables and parameters do not declare a type, and the type of each value is determined at runtime. This is the traditional behavior.
-
Static typing (opt-in): variables, parameters, process inputs/outputs, and workflow inputs/outputs can declare a type, which is checked at compile time. Static typing is enabled with the
nextflow.enable.typesfeature flag.
A type annotation is written after the name, separated by a colon:
def count: Integer = 0
def name: String = 'sample'
def reads: List<Path> = []
Nullable types
A type annotation can be suffixed with ? to indicate that the value may be null:
def x: String? = null
Generic type arguments
Some types are generic -- they are parameterized by one or more type arguments that specify the types of their contents. A type argument is written in angle brackets after the type name:
List<E>is a list whose elements have typeEChannel<E>is a channel whose values have typeEMap<K,V>is a map whose keys have typeKand values have typeV
For example:
// a list of strings
def sequences: List<String> = ['ATCG', 'GCTA']
// a map from string keys to integer values
def counts: Map<String,Integer> = [sample1: 1000, sample2: 1500]
// a channel of file paths
def ch_bams: Channel<Path> = channel.fromPath('*.bam')
Generic type arguments allow the type checker to verify that the contents of a collection or channel are used consistently -- for example, that a list of Path is not mistakenly treated as a list of String.
Record types
A record type is a user-defined type that specifies a set of named fields, each with its own type (see Syntax for the declaration grammar):
record Sample {
id: String
fastq_1: Path
fastq_2: Path?
}
A field can be marked as optional by appending ? to its type.
A record created with the record() function has the generic type Record and makes no guarantee about which fields it provides. A record type, by contrast, specifies a minimum set of required fields, and is used to make a stronger guarantee in a particular context, such as a workflow, process, or function input.
Record types are structural (or duck-typed): a record satisfies a record type if it provides at least the fields declared by that type. The record may have additional fields, and it does not matter how the record was created. A record is validated against a record type when it is supplied as an argument to a workflow, process, or function that declares a record type input, or cast to a record type with the as operator:
def hello(sample: Sample) {
println "Hello sample ${sample.id}!"
}
workflow {
// error: missing `fastq_1` field required by Sample
hello(record(id: '1', fastq_2: file('1_2.fastq')))
// ok: all required fields are present (extra fields are allowed)
hello(record(id: '1', fastq_1: file('1_1.fastq'), single_end: true))
}
Because matching is structural, two record types with the same fields and field types are interchangeable, even if they have different names.
Enum types
An enum type is a user-defined type with a fixed set of named values (see Syntax for the declaration grammar):
enum Day {
MONDAY,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
}
Each value is accessed as a property of the enum type:
def day = Day.MONDAY
An enum type is useful for representing a value that can take only one of a fixed set of options, in place of a free-form string.
Type casting
A value can be cast to a different type explicitly with the as operator. The value must be convertible to the target type, or an error is raised:
def x = '1h' as Duration
def n = '42' as Integer
A value is also cast implicitly in the following situations:
-
Numeric coercion -- when an arithmetic operation mixes an integer and a floating-point operand, the integer is coerced to a floating-point number.
-
Boolean coercion -- in a condition, such as if/else, assertions, or the logical operators, any value is coerced to a boolean according to its truthiness.
-
String coercion -- when a value is interpolated into a string, it is converted to a string with its
toString()method.