From 9031be94b312978328a3e4fba730b8476e66188f Mon Sep 17 00:00:00 2001 From: christopher-hakkaart Date: Fri, 5 Jan 2024 15:14:10 +0100 Subject: [PATCH] improve groovy --- docs/basic_training/groovy.md | 203 ++++++++++++++++++++++++------- docs/basic_training/modules.md | 12 +- docs/basic_training/operators.md | 83 ++++--------- 3 files changed, 191 insertions(+), 107 deletions(-) diff --git a/docs/basic_training/groovy.md b/docs/basic_training/groovy.md index 9b47e2f0..a520e51f 100644 --- a/docs/basic_training/groovy.md +++ b/docs/basic_training/groovy.md @@ -7,7 +7,7 @@ description: A quick intro to Groovy basic structures and idioms Nextflow is a domain specific language (DSL) implemented on top of the Groovy programming language, which in turn is a super-set of the Java programming language. This means that Nextflow can run any Groovy or Java code. -Here are some important Groovy syntax that are commonly used in Nextflow. +You have already been using some Groovy code in the previous sections, but now it's time to learn more about it. ## Printing values @@ -23,7 +23,7 @@ The only difference between the two is that the `println` method implicitly appe Parentheses for function invocations are optional. Therefore, the following syntax is also valid: - ```groovy linenums="1" + ```groovy linenums="1" title="snippet.nf" println "Hello, World!" ``` @@ -31,7 +31,7 @@ The only difference between the two is that the `println` method implicitly appe Comments use the same syntax as C-family programming languages: -```groovy linenums="1" +```groovy linenums="1" title="snippet.nf" // comment a single line /* @@ -44,7 +44,7 @@ Comments use the same syntax as C-family programming languages: To define a variable, simply assign a value to it: -```groovy linenums="1" +```groovy linenums="1" title="snippet.nf" x = 1 println x @@ -63,7 +63,7 @@ println x Local variables are defined using the `def` keyword: -```groovy linenums="1" +```groovy linenums="1" title="snippet.nf" def x = 'foo' ``` @@ -73,36 +73,71 @@ The `def` should be always used when defining variables local to a function or a A List object can be defined by placing the list items in square brackets: -```groovy linenums="1" +```groovy linenums="1" title="snippet.nf" list = [10, 20, 30, 40] + +println list ``` You can access a given item in the list with square-bracket notation (indexes start at `0`) or using the `get` method: -```groovy linenums="1" +```groovy linenums="1" title="snippet.nf" +list = [10, 20, 30, 40] + println list[0] println list.get(0) ``` In order to get the length of a list you can use the `size` method: -```groovy linenums="1" +```groovy linenums="1" title="snippet.nf" +list = [10, 20, 30, 40] + println list.size() ``` -We use the `assert` keyword to test if a condition is true (similar to an `if` function). Here, Groovy will print nothing if it is correct, else it will raise an AssertionError message. +You can use the `assert` keyword to test if a condition is true (similar to an `if` function). + +Here, Groovy will print nothing if it is correct, else it will raise an AssertionError message. + +```groovy linenums="1" title="snippet.nf" +list = [10, 20, 30, 40] -```groovy linenums="1" assert list[0] == 10 ``` -!!! note +!!! question "Exercise" This assertion should be correct, try changing it to an incorrect one. + ```groovy linenums="1" title="snippet.nf" + list = [10, 20, 30, 40] + + assert list[0] == 10 + ``` + + ??? Solution + + Your solution could look something similar to this: + + ```groovy linenums="1" title="snippet.nf" + list = [10, 20, 30, 40] + + assert list[0] == 11 + ``` + + You should see an error message similar to this: + + ```console title="Output" + ERROR ~ assert list[0] == 11 + | | + | 10 + [10, 20, 30, 40] + ``` + Lists can also be indexed with negative indexes and reversed ranges. -```groovy linenums="1" +```groovy linenums="1" title="snippet.nf" list = [0, 1, 2] assert list[-1] == 2 assert list[-1..0] == list.reverse() @@ -114,7 +149,7 @@ assert list[-1..0] == list.reverse() List objects implement all methods provided by the [java.util.List](https://docs.oracle.com/javase/8/docs/api/java/util/List.html) interface, plus the extension methods provided by [Groovy](http://docs.groovy-lang.org/latest/html/groovy-jdk/java/util/List.html). -```groovy linenums="1" +```groovy linenums="1" title="snippet.nf" assert [1, 2, 3] << 1 == [1, 2, 3, 1] assert [1, 2, 3] + [1] == [1, 2, 3, 1] assert [1, 2, 3, 1] - [1] == [2, 3] @@ -136,7 +171,7 @@ assert [4, 2, 1, 3].findAll { it % 2 == 0 } == [4, 2] Maps are like lists that have an arbitrary key instead of an integer. Therefore, the syntax is very much aligned. -```groovy linenums="1" +```groovy linenums="1" title="snippet.nf" map = [a: 0, b: 1, c: 2] ``` @@ -146,7 +181,9 @@ Maps can be accessed in a conventional square-bracket syntax or as if the key wa Click the :material-plus-circle: icons in the code for explanations. -```groovy linenums="1" +```groovy linenums="1" title="snippet.nf" +map = [a: 0, b: 1, c: 2] + assert map['a'] == 0 // (1)! assert map.b == 1 // (2)! assert map.get('c') == 2 // (3)! @@ -158,7 +195,9 @@ assert map.get('c') == 2 // (3)! To add data or to modify a map, the syntax is similar to adding values to a list: -```groovy linenums="1" +```groovy linenums="1" title="snippet.nf" +map = [a: 0, b: 1, c: 2] + map['a'] = 'x' // (1)! map.b = 'y' // (2)! map.put('c', 'z') // (3)! @@ -175,13 +214,14 @@ Map objects implement all methods provided by the [java.util.Map](https://docs.o String literals can be defined by enclosing them with either _single-_ ('') or _double-_ ("") quotation marks. -```groovy linenums="1" +```groovy linenums="1" title="snippet.nf" foxtype = 'quick' foxcolor = ['b', 'r', 'o', 'w', 'n'] println "The $foxtype ${foxcolor.join()} fox" x = 'Hello' -println '$x + $y' +y = 'World' +println '$x $y' ``` ```console title="Output" @@ -194,13 +234,34 @@ $x + $y Note the different use of `$` and `${..}` syntax to interpolate value expressions in a string literal. The `$x` variable was _not_ expanded, as it was enclosed by single quotes. +!!! question "Exercise" + + Modify the script above to print `Hello World` instead of `$x + $y`. + + ??? Solution + + Modify `println '$x $y'` to `println "$x $y"`. + + ```groovy linenums="1" title="snippet.nf" + + ```groovy linenums="1" title="snippet.nf" + foxtype = 'quick' + foxcolor = ['b', 'r', 'o', 'w', 'n'] + println "The $foxtype ${foxcolor.join()} fox" + + x = 'Hello' + y = 'World' + println "$x $y" + ``` + Finally, string literals can also be defined using the `/` character as a delimiter. They are known as **slashy** strings and are useful for defining regular expressions and patterns, as there is no need to escape backslashes. As with double-quote strings they allow to interpolate variables prefixed with a `$` character. -Try the following to see the difference: +See the difference below: -```groovy linenums="1" +```groovy linenums="1" title="snippet.nf" x = /tic\tac\toe/ y = 'tic\tac\toe' +z = "tic\tac\toe" println x println y @@ -223,6 +284,11 @@ text = """ println text ``` +```console title="Output" +Hello there James. +How are you today? +``` + Finally, multi-line strings can also be defined with slashy strings. For example: ```groovy linenums="1" @@ -234,13 +300,19 @@ text = / println text ``` +```console title="Output" +This is a multi-line +slashy string! +It's cool, isn't it?! +``` + !!! info Like before, multi-line strings inside double quotes and slash characters support variable interpolation, while single-quoted multi-line strings do not. ## If statement -The `if` statement uses the same syntax common in other programming languages, such as Java, C, JavaScript, etc. +The `if` statement uses the same syntax common in other programming languages, such as Java, C, and JavaScript. ```groovy linenums="1" if (< boolean expression >) { @@ -259,6 +331,10 @@ if (x > 10) println 'Hello' ``` +```console title="Output" +Hello +``` + !!! tip `null`, empty strings, and empty collections are evaluated to `false`. @@ -289,23 +365,48 @@ if (x > 10) !!! tip - In some cases it can be useful to replace the `if` statement with a ternary expression (aka conditional expression). For example: + In some cases it can be useful to replace the `if` statement with a ternary expression (aka a conditional expression): ```groovy linenums="1" println list ? list : 'The list is empty' ``` - The previous statement can be further simplified using the [Elvis operator](http://groovy-lang.org/operators.html#_elvis_operator), as shown below: + The previous statement can be further simplified using the [Elvis operator](http://groovy-lang.org/operators.html#_elvis_operator): ```groovy linenums="1" println list ?: 'The list is empty' ``` +!!! + + Write an if statement that prints `Hello` if the variable `x` is greater than 10 and `Goodbye` if it is less than 10. + + ??? Solution + + Your solution could look something similar to this: + + ```groovy linenums="1" title="snippet.nf" + x = 11 + + if (x > 10) + println 'Hello' + else + println 'Goodbye' + ``` + + Or this: + + ```groovy linenums="1" title="snippet.nf" + x = 11 + + println x > 10 ? 'Hello' : 'Goodbye' + ``` + ## For statement -The classical `for` loop syntax is supported as shown here: +The classical `for` loop syntax is supported: -```groovy linenums="1" +```groovy linenums="1" title="snippet.nf" for (int i = 0; i < 3; i++) { println("Hello World $i") } @@ -313,7 +414,7 @@ for (int i = 0; i < 3; i++) { Iteration over list objects is also possible using the syntax below: -```groovy linenums="1" +```groovy linenums="1" title="snippet.nf" list = ['a', 'b', 'c'] for (String elem : list) { @@ -323,9 +424,9 @@ for (String elem : list) { ## Functions -It is possible to define a custom function into a script, as shown here: +It is possible to define a custom function into a script: -```groovy linenums="1" +```groovy linenums="1" title="snippet.nf" def fib(int n) { return n < 2 ? 1 : fib(n - 1) + fib(n - 2) } @@ -333,7 +434,9 @@ def fib(int n) { assert fib(10)==89 ``` -A function can take multiple arguments separating them with a comma. The `return` keyword can be omitted and the function implicitly returns the value of the last evaluated expression. Also, explicit types can be omitted, though not recommended: +A function can take multiple arguments separating them with a comma. + +The `return` keyword can be omitted and the function implicitly returns the value of the last evaluated expression. Also, explicit types can be omitted, though not recommended: ```groovy linenums="1" def fact(n) { @@ -345,37 +448,43 @@ assert fact(5) == 120 ## Closures -Closures are the Swiss army knife of Nextflow/Groovy programming. In a nutshell, a closure is a block of code that can be passed as an argument to a function. A closure can also be used to define an anonymous function. +Closures are the Swiss army knife of Nextflow/Groovy programming. In a nutshell, a closure is a block of code that can be passed as an argument to a function. + +A closure can also be used to define an anonymous function. More formally, a closure allows the definition of functions as first-class objects. -```groovy linenums="1" +```groovy linenums="1" title="snippet.nf" square = { it * it } ``` The curly brackets around the expression `it * it` tells the script interpreter to treat this expression as code. The `it` identifier is an implicit variable that represents the value that is passed to the function when it is invoked. -Once compiled, the function object is assigned to the variable `square` as any other variable assignment shown previously. To invoke the closure execution use the special method `call` or just use the round parentheses to specify the closure parameter(s). For example: +Once compiled, the function object is assigned to the variable `square` as any other variable assignment shown previously. -```groovy linenums="1" +To invoke the closure execution use the special method `call` or just use the round parentheses to specify the closure parameter(s): + +```groovy linenums="1" title="snippet.nf" assert square.call(5) == 25 assert square(9) == 81 ``` As is, this may not seem interesting, but we can now pass the `square` function as an argument to other functions or methods. Some built-in functions take a function like this as an argument. One example is the `collect` method on lists: -```groovy linenums="1" +```groovy linenums="1" title="snippet.nf" x = [1, 2, 3, 4].collect(square) println x ``` -```console title="Output" +```console linenums="1" title="Output" [1, 4, 9, 16] ``` -By default, closures take a single parameter called `it`. To give it a different name use the `->` syntax. For example: +By default, closures take a single parameter called `it`. -```groovy linenums="1" +To give it a different name use the `->` syntax. For example: + +```groovy linenums="1" title="snippet.nf" square = { num -> num * num } ``` @@ -383,12 +492,16 @@ It’s also possible to define closures with multiple, custom-named parameters. For example, when the method `each()` is applied to a map it can take a closure with two arguments, to which it passes the _key-value_ pair for each entry in the `map` object. For example: -```groovy linenums="1" -printMap = { a, b -> println "$a with value $b" } -values = ["Yue": "Wu", "Mark": "Williams", "Sudha": "Kumari"] -values.each(printMap) +```groovy linenums="1" title="snippet.nf" +printMap = { a, b -> println "$a with value $b" } // (1)! +values = ["Yue": "Wu", "Mark": "Williams", "Sudha": "Kumari"] // (2)! +values.each(printMap) // (3)! ``` +1. Closure object that prints the key-value pair. +2. Defines a map object with three entries. +3. Invokes the `each` method passing the closure object. + ```console title="Output" Yue with value Wu Mark with value Williams @@ -399,11 +512,11 @@ A closure has two other important features. First, it can access and _modify_ variables in the scope where it is defined. -Second, a closure can be defined in an _anonymous_ manner, meaning that it is not given a name, and is defined in the place where it needs to be used. +Second, a closure can be defined in an _anonymous_ manner, meaning that it is not given a name, and is only defined in the place where it needs to be used. As an example showing both these features, see the following code fragment: -```groovy linenums="1" +```groovy linenums="1" title="snippet.nf" result = 0 // (1)! values = ["China": 1, "India": 2, "USA": 3] // (2)! values.keySet().each { result += values[it] } // (3)! @@ -414,6 +527,10 @@ println result 2. Defines a map object. 3. Invokes the `each` method passing the closure object which modifies the `result` variable. +```console title="Output" +6 +``` + Learn more about closures in the [Groovy documentation](http://groovy-lang.org/closures.html). ## More resources diff --git a/docs/basic_training/modules.md b/docs/basic_training/modules.md index 86db9471..1c3a53c6 100644 --- a/docs/basic_training/modules.md +++ b/docs/basic_training/modules.md @@ -14,9 +14,9 @@ Nextflow DSL2 allows for the definition of stand-alone module scripts that can b ### Importing modules -Components defined in the module script can be imported into other Nextflow scripts using the `include` statement. This allows you to store these components in a separate file(s) so that they can be re-used in multiple workflows. +Components defined in the module script can be imported into other Nextflow scripts using the `include` statement. This allows you to store these components in one or more file(s) that they can be re-used in multiple workflows. -Using the `hello.nf` example, we can achieve this by: +Using the `hello.nf` example, you can achieve this by: - Creating a file called `modules.nf` in the top-level directory. - Copying and pasting the two process definitions for `SPLITLETTERS` and `CONVERTTOUPPER` into `modules.nf`. @@ -86,7 +86,7 @@ include { CONVERTTOUPPER } from './modules.nf' } ``` - We now have modularized processes which makes the code reusable. + You now have modularized processes which makes the code reusable. ### Multiple imports @@ -154,7 +154,7 @@ workflow { Nextflow allows the use of alternative output definitions within workflows to simplify your code. -In the previous basic example (`hello.nf`), we defined the channel names to specify the input to the next process: +In the previous basic example (`hello.nf`), you defined the channel names to specify the input to the next process: ```groovy linenums="1" workflow { @@ -167,9 +167,9 @@ workflow { !!! note - We have moved the `greeting_ch` into the workflow scope for this exercise. + You have moved the `greeting_ch` into the workflow scope for this exercise. -We can also explicitly define the output of one channel to another using the `.out` attribute, removing the channel definitions completely: +You can also explicitly define the output of one channel to another using the `.out` attribute, removing the channel definitions completely: ```groovy linenums="1" hl_lines="3-5" workflow { diff --git a/docs/basic_training/operators.md b/docs/basic_training/operators.md index 49d53cd2..b1caf142 100644 --- a/docs/basic_training/operators.md +++ b/docs/basic_training/operators.md @@ -544,7 +544,7 @@ Channel ### `splitJson()` -We can also easily parse the JSON file format using the `splitJson` channel operator. +You can parse the JSON file format using the `splitJson` channel operator. The `splitJson` operator supports JSON arrays: @@ -552,51 +552,48 @@ The `splitJson` operator supports JSON arrays: Channel .of('["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]') .splitJson() - .view { "Item: ${it}" } + .view() ``` ```console title="Output" -Item: Sunday -Item: Monday -Item: Tuesday -Item: Wednesday -Item: Thursday -Item: Friday -Item: Saturday +Sunday +Monday +Tuesday +Wednesday +Thursday +Friday +Saturday ``` +As well as JSON arrays in objects: + ```groovy linenums="1" title="snippet.nf" Channel .of('{"player": {"name": "Bob", "height": 180, "champion": false}}') .splitJson() - .view { "Item: ${it}" } + .view() ``` ```console title="Output" -Item: [key:player, value:[name:Bob, height:180, champion:false]] +[value:[name:Bob, height:180, champion:false], key:player] ``` -And even a JSON array of JSON objects! +And even a JSON array of JSON objects: ```groovy linenums="1" title="snippet.nf" Channel .of('[{"name": "Bob", "height": 180, "champion": false}, \ {"name": "Alice", "height": 170, "champion": false}]') .splitJson() - .view { "Item: ${it}" } + .view() ``` ```console title="Output" -Item: [name:Bob, height:180, champion:false] -Item: [name:Alice, height:170, champion:false] +[name:Bob, height:180, champion:false] +[name:Alice, height:170, champion:false] ``` -```groovy linenums="1" title="snippet.nf" -Channel - .fromPath('file.json') - .splitJson() - .view { "Item: ${it}" } -``` +You can also parse JSON files directly: ```json title="file.json" [ @@ -605,46 +602,16 @@ Channel ] ``` -```console title="Output" -Item: [name:Bob, height:180, champion:false] -Item: [name:Alice, height:170, champion:false] -``` - -### YAML - -This can also be used as a way to parse YAML files: - ```groovy linenums="1" title="snippet.nf" -import org.yaml.snakeyaml.Yaml - -def f = file('data/meta/regions.yml') -def records = new Yaml().load(f) - - -for (def entry : records) { - log.info "$entry.patient_id -- $entry.feature" -} -``` - -```yaml title="regions.yml" ---8<-- "nf-training/data/meta/regions.yml" +Channel + .fromPath('file.json') + .splitJson() + .view() ``` -```console -ATX-TBL-001-GB-01-105 -- pass_vafqc_flag -ATX-TBL-001-GB-01-105 -- pass_stripy_flag -ATX-TBL-001-GB-01-105 -- pass_manual_flag -ATX-TBL-001-GB-01-105 -- other_region_selection_flag -ATX-TBL-001-GB-01-105 -- ace_information_gained -ATX-TBL-001-GB-01-105 -- concordance_flag -ATX-TBL-001-GB-01-105 -- pass_vafqc_flag -ATX-TBL-001-GB-01-105 -- pass_stripy_flag -ATX-TBL-001-GB-01-105 -- pass_manual_flag -ATX-TBL-001-GB-01-105 -- other_region_selection_flag -ATX-TBL-001-GB-01-105 -- ace_information_gained -ATX-TBL-001-GB-01-105 -- concordance_flag -ATX-TBL-001-GB-01-105 -- pass_vafqc_flag -ATX-TBL-001-GB-01-105 -- pass_stripy_flag +```console title="Output" +[name:Bob, height:180, champion:false] +[name:Alice, height:170, champion:false] ``` ## More resources