Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Groovy Roadmap

Groovy Roadmap

Groovy 4 was released in early 2022. It includes switch expressions, sealed types, records, additional module re-working, bundled type checker extensions, bundled macro methods, a JavaShell, some additional AST transformations, and a myriad of other new miscellaneous features.

4578d99b560b2a470e05288ef6766ac2?s=128

paulking

May 03, 2022
Tweet

More Decks by paulking

Other Decks in Programming

Transcript

  1. objectcomputing.com © 2018, Object Computing, Inc. (OCI). All rights reserved.

    No part of these notes may be reproduced, stored in a retrieval system, or transmitted, in any form or by any means, electronic, mechanical, photocopying, recording, or otherwise, without the prior, written permission of Object Computing, Inc. (OCI) Groovy 4 Update Presented by Dr Paul King Object Computing & VP Apache Groovy © 2022 Object Computing, Inc. (OCI). All rights reserved. See also: https://github.com/paulk-asert/upcoming-groovy/tree/master/Four
  2. Groovy • Multi-faceted programming language • Static and dynamic natures

    • Strengths: • Extensibility including runtime and compile-time metaprogramming • Java-like syntax and integration • Domain Specific Language support • Scripting
  3. Groovy status in 2022: Healthy • > 600K lines of

    source code • > 19K commits • > 8K issues & enhancements resolved • > 500 contributors • > 200 releases • > 1B downloads and accelerating
  4. Parrot Parser • Lambdas • Method references • New operators

    & Safe index ===, !===, ?=, !in, !instanceof • Additional Java compatibility GDK enhancements • average(), takeBetween(), shuffle(), … AST transforms • @NullCheck • @Groovydoc Libraries/Tooling • Split package remediation • Java module system improvements • JSR-308 Annotation improvements • YAML builder/slurper Groovy 3 - Summary
  5. Consolidation & Structuring • Maven coordinates • Module changes •

    Indy only, Parrot only • ~33% smaller zip • ~10% smaller core jar Language Features • Switch expressions • Records & Sealed types • Language integrated query • Improved type annotations Libraries/Tooling • Built-in type checkers • Built-in macro methods • TOML builder/slurper • JavaShell • Improved ranges AST transforms • @POJO • @RecordType • Groovy Contracts GDK enhancements Groovy 4 - Summary
  6. Consolidation & Structuring • Maven coordinates • Module changes •

    Indy only, Parrot only • ~33% smaller zip • ~10% smaller core jar Language Features • Switch expressions • Records & Sealed types • Language integrated query • Improved type annotations Libraries/Tooling • Built-in type checkers • Built-in macro methods • TOML builder/slurper • JavaShell • Improved ranges AST transforms • @POJO • @RecordType • Groovy Contracts GDK enhancements Groovy 4 - Summary
  7. Important naming/structuring changes Maven coordinate change org.codehaus.groovy org.apache.groovy

  8. Important naming/structuring changes Maven coordinate change org.codehaus.groovy org.apache.groovy Note: Doesn’t

    imply all internal package names have been changed. That is a parallel and on-going activity which we do as opportunities arise. We don’t break backwards compatibility when we don’t have to.
  9. Module changes Removed modules groovy-bsf groovy-jaxb New optional modules groovy-contracts

    groovy-ginq groovy-macro-library groovy-toml groovy-typecheckers Module changes for groovy-all groovy-testng: included in all optional groovy-yaml: optional included in all
  10. Module changes Split packaging legacy package removal groovy-xml: Also: groovy-ant,

    groovy-swing, groovy-test, … More details: https://groovy-lang.org/releasenotes/groovy-3.0.html#Groovy3.0releasenotes-Splitpackages 2.5 3.0 4.0 groovy.util.XmlParser groovy.util.XmlSlurper groovy.util.XmlParser groovy.util.XmlSlurper groovy.xml.XmlParser groovy.xml.XmlSlurper groovy.xml.XmlParser groovy.xml.XmlSlurper
  11. Legacy consolidation Old parser removal Antlr 2 Antlr4 Classic bytecode

    generation removal Classic Indy
  12. Consolidation & Structuring • Maven coordinates • Module changes •

    Indy only, Parrot only • ~33% smaller zip • ~10% smaller core jar Language Features • Switch expressions • Records & Sealed types • Language integrated query • Improved type annotations Libraries/Tooling • Built-in type checkers • Built-in macro methods • TOML builder/slurper • JavaShell • Improved ranges AST transforms • @POJO • @RecordType • Groovy Contracts GDK enhancements Groovy 4 - Summary
  13. Switch expressions def a = 9 def result = switch(a)

    { case 6, 8 -> 'b' case 9 -> 'c' default -> 'z' } assert 'c' == result
  14. Switch expression enhancements enum Day { Sunday, Monday, Tuesday, Wednesday,

    Thursday, Friday, Saturday } import static Day.* def isWeekend(Day d) { switch(d) { case Monday..Friday -> false case [Sunday, Saturday] -> true } } assert [Sunday, Monday, Friday].collect{ isWeekend(it) } == [true, false, false]
  15. Switch expression: yield variation enum Day { Sunday, Monday, Tuesday,

    Wednesday, Thursday, Friday, Saturday } import static Day.* def isWeekend(Day d) { return switch(d) { case Monday..Friday: yield false case { it.toString()[0] == 'S' }: yield true } } assert [Sunday, Monday, Friday].collect{ isWeekend(it) } == [true, false, false]
  16. Switch expression: differences to Java Java Groovy JDK versions 14+

    (12/13 preview) 8+ “ -> ” syntax (switch rule)   “ : … yield” syntax   Supported case selector expressions Constant • Boolean, Number, String Enum constant Constant • Boolean, Number, String, null Enum constant List expression Range expression Closure Regex Pattern Class expression Enhanced switch (17-19 preview) Type pattern Guard/when Null Class or Closure Closure already supported Extensible via isCase  
  17. Records • Shorthand for data classes (immutable DTOs) • Produces

    a class that: • Is implicitly final • Has a private final field for each property, e.g. color • Has an accessor method for each property of the same name, e.g. color() • Has a default Point(int, int, String) constructor • Has a default serialVersionUID of 0L • Has implicit toString(), equals() and hashCode() methods record Point(int x, int y, String color) { } def blueOrigin = new Point(0, 0, 'Blue') Incubating
  18. @RecordType Alternative style for: @RecordType class Cyclist { String firstName

    String lastName } def richie = new Cyclist('Richie', 'Porte') Incubating record Cyclist(String firstName, String lastName) { }
  19. Records: Groovy enhancements record Material(String name, int strength, String warranty,

    String color) { } def straw = new Material('Straw', 4, 'light winds only', 'yellow') assert straw.toList() == ['Straw', 4, 'light winds only', 'yellow'] assert straw[1] == 4 assert straw.size() == 4 Incubating
  20. Records: Groovy enhancements record Material(String name, int strength, String warranty,

    String color) { } def straw = new Material('Straw', 4, 'light winds only', 'yellow') assert straw.toList() == ['Straw', 4, 'light winds only', 'yellow'] assert straw[1] == 4 assert straw.size() == 4 Incubating
  21. Records: Groovy enhancements record Material(String name, int strength, String warranty,

    String color) { } def straw = new Material('Straw', 4, 'light winds only', 'yellow') assert straw.toList() == ['Straw', 4, 'light winds only', 'yellow'] assert straw[1] == 4 assert straw.size() == 4 def sticks = new Material(name: 'Sticks', strength: 42, warranty: 'light winds', color: 'brown') assert sticks.toMap() == [name: 'Sticks', strength: 42, warranty: 'light winds', color: 'brown'] assert sticks[3] == 'brown' Incubating
  22. Records: Groovy enhancements @RecordOptions(copyWith = true, components = true) record

    Material(String name, int strength, String warranty, String color) { } def straw = new Material('Straw', 4, 'light winds only', 'yellow') assert straw.toList() == ['Straw', 4, 'light winds only', 'yellow'] assert straw[1] == 4 assert straw.size() == 4 def sticks = new Material(name: 'Sticks', strength: 42, warranty: 'light winds', color: 'brown') assert sticks.toMap() == [name: 'Sticks', strength: 42, warranty: 'light winds', color: 'brown'] assert sticks[3] == 'brown' def bricks = sticks.copyWith(name: 'Bricks', color: 'red', warranty: 'heavy winds') assert bricks.components() instanceof Tuple4 assert bricks.components().toList() == ['Bricks', 42, 'heavy winds', 'red'] Incubating
  23. Native vs emulated Records @RecordOptions(mode=RecordTypeMode.AUTO) // default, not needed record

    Person(String name, int age) {} @RecordOptions(mode=RecordTypeMode.NATIVE) record Point(int x, int y, String color) { } @RecordOptions(mode=RecordTypeMode.EMULATE) @RecordType class Cyclist { String firstName String lastName } Regardless of keyword or annotation style, native or emulated records can be selected: • AUTO – native if JDK16+ • NATIVE – record info in bytecode, error if JDK < 16 • EMULATE – record info captured as annotations (works on JDK8+ but won’t be seen as records by Java)
  24. Records: differences to Java Java Record Groovy Emulated Record Groovy

    Native Record JDK version 16+ 8+ 16+ Serialization Record spec Traditional Record spec Recognized by Java, Groovy Groovy Java, Groovy Standard features • accessors • tuple constructor • toString, equals, hashCode    Optional enhancements  toMap, toList, size, getAt, components, copyWith, named-arg constructor Customisable via coding    Customisable via AST transforms (declarative)   
  25. Record-like functionality: summary Accessors toString() equals() hashCode() Constructor Groovy version

    JDK version Standard Groovy JavaBean From Object From Object Named-arg 1.0+ 6+ @Immutable JavaBean From properties From properties Tuple and named-arg 1.7+ 6+ @ToString - From properties - - 1.8+ 6+ @EqualsAnd HashCode - - From properties - 1.8+ 6+ @Canonical JavaBean From properties From properties Tuple and named-arg 1.8+ 6+ Emulated & native records Record From components From components Tuple and named-arg 4.0+ 8+ & 16+
  26. Sealed Type Motivation • Inheritance is a powerful abstraction for

    building systems class Shape { … } class Square extends Shape { … } final class Circle extends Shape { … } Incubating
  27. Sealed Type Motivation • Inheritance is a powerful abstraction for

    building systems • There are scenarios where limiting inheritance has benefits • Less defensive programming in parent classes • To support additional compiler checks, e.g. pattern matching/casts • Traditional mechanisms for limiting inheritance are crude • Using final stops all inheritance (not applicable to interfaces) • Package-private parent classes don’t provide an accessible parent abstraction (not applicable to interfaces)
  28. Sealed Type Motivation • Inheritance is a powerful abstraction for

    building systems • There are scenarios where limiting inheritance has benefits • Less defensive programming in parent classes • To support additional compiler checks, e.g. pattern matching/casts • Traditional mechanisms for limiting inheritance are crude • Using final stops all inheritance (not applicable to interfaces) • Package-private parent classes don’t provide an accessible parent abstraction (not applicable to interfaces) • Sealed type • Provides a fixed set of children rather than all or nothing • Decouples accessibility from extendibility • Easier to add new methods, harder to add new types • Unsealed type • Easy to add new types, harder to add new methods
  29. Sealed Type Motivation abstract class ShellOp { abstract String getOp()

    def calc(int one, int two) { // powerful approach - use with care new GroovyShell().evaluate("$one $op $two") } } class Multiply extends ShellOp { String op = "*" } class Add extends ShellOp { String op = "+" } def ops = [new Add(), new Multiply()] assert ops*.calc(40, 2) == [42, 80]
  30. Sealed Type Motivation abstract class ShellOp { abstract String getOp()

    def calc(int one, int two) { // powerful approach - use with care new GroovyShell().evaluate("$one $op $two") } } class Multiply extends ShellOp { String op = "*" } class Add extends ShellOp { String op = "+" } def ops = [new Add(), new HackedAdd(), new Multiply()] assert ops*.calc(40, 2) == [42, 42, 80] class HackedAdd extends ShellOp { String op = "+ (println(new File('/etc/passwd').text) ?: 0) +" } • Contrived code injection • Could be hardened in various ways
  31. Sealed Type Motivation sealed abstract class ShellOp permits Multiply, Add

    { abstract String getOp() int calc(int one, int two) { new GroovyShell().evaluate("$one $op $two") } } class Multiply extends ShellOp { String op = "*" } class Add extends ShellOp { String op = "+" } def ops = [new Add(), new Multiply()] assert ops*.calc(40, 2) == [42, 80] • Sealed hierarchy also limits potential for such scenarios
  32. Sealed Type Motivation abstract class Material { abstract String getColor()

    String toString() { "${getClass().name} with color ${color.toLowerCase()}" } } class Straw extends Material { String color = "Yellow" } class Wood extends Material { String color = "Brown" } class Brick extends Material { String color = "Red" } def materials = [new Straw(), new Wood(), new Brick()] assert materials*.toString() == [ 'Straw with color yellow', 'Wood with color brown', 'Brick with color red' ] • More frequently implicit or hidden implementation details or assumptions
  33. Sealed Type Motivation abstract class Material { abstract String getColor()

    String toString() { "${getClass().name} with color ${color.toLowerCase()}" } } class Straw extends Material { String color = "Yellow" } class Wood extends Material { String color = "Brown" } class Brick extends Material { String color = "Red" } def materials = [new Straw(), new Wood(), new Brick()] assert materials*.toString() == [ 'Straw with color yellow', 'Wood with color brown', 'Brick with color red' ] class Glass extends Material { String color = null // transparent }
  34. Sealed Type Motivation abstract class Material { abstract String getColor()

    String toString() { "${getClass().name} with color ${color.toLowerCase()}" } } class Straw extends Material { String color = "Yellow" } class Wood extends Material { String color = "Brown" } class Brick extends Material { String color = "Red" } def materials = [new Straw(), new Wood(), new Brick()] assert materials*.toString() == [ 'Straw with color yellow', 'Wood with color brown', 'Brick with color red' ] class Glass extends Material { String color = null // transparent } new Glass().toString() // NullPointerException • Classes added later may yield unexpected results
  35. Sealed Type Motivation sealed abstract class Material permits Straw, Wood,

    Brick { abstract String getColor() String toString() { "${getClass().name} with color ${color.toLowerCase()}" } } class Straw extends Material { String color = "Yellow" } class Wood extends Material { String color = "Brown" } class Brick extends Material { String color = "Red" } def materials = [new Straw(), new Wood(), new Brick()] assert materials*.toString() == [ 'Straw with color yellow', 'Wood with color brown', 'Brick with color red' ] • Could be hardened in various ways or again use sealed
  36. Sealed Type Motivation abstract class Material { String warranty() {

    switch(this) { case Straw -> 'Use only in light winds' case Wood -> 'Okay for light breezes' case Brick -> 'Withstands huffing and puffing' } } } class Straw extends Material { } class Wood extends Material { } class Brick extends Material { } def materials = [new Straw(), new Wood(), new Brick()] assert materials*.warranty() == [ 'Use only in light winds', 'Okay for light breezes', 'Withstands huffing and puffing' ] • Switch expressions are a special case
  37. Sealed Type Motivation abstract class Material { String warranty() {

    switch(this) { case Straw -> 'Use only in light winds' case Wood -> 'Okay for light breezes' case Brick -> 'Withstands huffing and puffing' } } } class Straw extends Material { } class Wood extends Material { } class Brick extends Material { } def materials = [new Straw(), new Wood(), new Brick()] assert materials*.warranty() == [ 'Use only in light winds', 'Okay for light breezes', 'Withstands huffing and puffing' ] class Glass extends Material { } new Glass().warranty() // null (or error) • Switch expressions are a special case
  38. Sealed Type Motivation sealed abstract class Material permits Straw, Wood,

    Brick { String warranty() { switch(this) { case Straw -> 'Use only in light winds' case Wood -> 'Okay for light breezes' case Brick -> 'Withstands huffing and puffing' } } } class Straw extends Material { } class Wood extends Material { } class Brick extends Material { } def materials = [new Straw(), new Wood(), new Brick()] assert materials*.warranty() == [ 'Use only in light winds', 'Okay for light breezes', 'Withstands huffing and puffing' ] class Glass extends Material { } // × compile error • This becomes a compile-time error which forces it to be handled
  39. Sealed Types (more details) @Sealed(permittedSubclasses=[Diamond,Circle]) class Shape { } final

    class Diamond extends Shape { } final class Circle extends Shape { } • Class or abstract class • Annotation style
  40. Sealed Types (more details) @Sealed(permittedSubclasses=[Diamond,Circle]) class Shape { } final

    class Diamond extends Shape { } final class Circle extends Shape { } sealed trait Triangle permits Equilateral, Isosceles { } final class Equilateral implements Triangle { } final class Isosceles implements Triangle { } • Trait • Keyword style
  41. Sealed Types (more details) @Sealed(permittedSubclasses=[Diamond,Circle]) class Shape { } final

    class Diamond extends Shape { } final class Circle extends Shape { } sealed trait Triangle permits Equilateral, Isosceles { } final class Equilateral implements Triangle { } final class Isosceles implements Triangle { } sealed interface Polygon { } final class Square implements Polygon { } final class Rectangle implements Polygon { } • Interface • Keyword style • Inferred subclasses
  42. Sealed Types (more details) @Sealed(permittedSubclasses=[Diamond,Circle]) class Shape { } final

    class Diamond extends Shape { } final class Circle extends Shape { } sealed trait Triangle permits Equilateral, Isosceles { } final class Equilateral implements Triangle { } final class Isosceles implements Triangle { } sealed interface Polygon { } final class Square implements Polygon { } final class Rectangle implements Polygon { } You can choose either keyword or annotation style • Annotation style might be useful when using older tools (IDEs, CodeNarc) • Keyword style generally preferred
  43. Native vs emulated Sealed Types @SealedOptions(mode=SealedMode.AUTO) // default, not needed

    @Sealed(permittedSubclasses=[Diamond,Circle]) class Shape { } final class Diamond extends Shape { } final class Circle extends Shape { } @SealedOptions(mode=SealedMode.NATIVE) sealed trait Triangle permits Equilateral, Isosceles { } final class Equilateral implements Triangle { } final class Isosceles implements Triangle { } @SealedOptions(mode=SealedMode.EMULATE) sealed interface Polygon { } final class Square implements Polygon { } final class Rectangle implements Polygon { } Regardless of keyword or annotation style, native or emulated sealed types can be selected • AUTO – native if JDK17+ • NATIVE – sealed info in bytecode, error if JDK < 17 • EMULATE – sealed info captured as annotations (works on JDK8+ but won’t be seen as sealed by Java)
  44. Sealed Types – Good for ADTs @Sealed interface Tree<T> {}

    @Singleton final class Empty implements Tree { String toString() { 'Empty' } } @Canonical final class Node<T> implements Tree<T> { T value Tree<T> left, right } Tree<Integer> tree = new Node<>(42, new Node<>(0, Empty.instance, Empty.instance), Empty.instance) assert tree.toString() == 'Node(42, Node(0, Empty, Empty), Empty)'
  45. Sealed Types – Hybrid hierarchies sealed class Shape permits Circle,

    Polygon, Rectangle { } final class Circle extends Shape { } non-sealed class Polygon extends Shape { } final class Pentagon extends Polygon { } sealed class Rectangle extends Shape permits Square { } final class Square extends Rectangle { }
  46. sealed class Shape permits Circle, Polygon, Rectangle { } final

    class Circle extends Shape { } non-sealed class Polygon extends Shape { } final class Pentagon extends Polygon { } sealed class Rectangle extends Shape permits Square { } final class Square extends Rectangle { } Sealed Types – Hybrid hierarchies Shape Circle Polygon Rectangle Pentagon Square
  47. Sealed Types – Hybrid hierarchies • Groovy follows Scala style

    of non- sealed being optional • We envisage a future CodeNarc rule which could enforce the Java style sealed class Shape permits Circle, Polygon, Rectangle { } final class Circle extends Shape { } non-sealed class Polygon extends Shape { } final class Pentagon extends Polygon { } sealed class Rectangle extends Shape permits Square { } final class Square extends Rectangle { }
  48. Sealed types: differences to Java Java Sealed Type Groovy Emulated

    Sealed Type Groovy Native Sealed Type JDK version 17+ 8+ 17+ Recognized by Java, Groovy Groovy Java, Groovy “non-sealed” to reopen Required Optional Optional
  49. Language Integrated Query (GQ, GINQ) • Language level support for

    SQL-like query expressions across aggregations • Currently traditional collections including e.g. parsed JSON • Later SQL databases too Incubating
  50. • Projection • Filtering • Joins • Aggregations • Sorting

    • Grouping • Pagination • Nesting • Windows GINQ from p in persons leftjoin c in cities on p.city.name == c.name where c.name == 'Shanghai' select p.name, c.name as cityName from p in persons groupby p.gender having p.gender == 'Male' select p.gender, max(p.age) from p in persons orderby p.age in desc, p.name select p.name from n in numbers where n > 0 && n <= 3 select n * 2 from n1 in nums1 innerjoin n2 in nums2 on n1 == n2 select n1 + 1, n2
  51. Language Integrated Query Motivation record Person(String name, int age) {}

    def people = [new Person('Daniel', 35), new Person('Linda', 25), new Person('Peter', 45)] assert [['Linda', 25], ['Daniel', 35]] == people .findAll { p -> p.age < 40 } .sort { p -> p.age } .collect { p -> [p.name, p.age] } • Map, filter, reduce style using Closures
  52. Language Integrated Query Motivation record Person(String name, int age) {}

    def people = [new Person('Daniel', 35), new Person('Linda', 25), new Person('Peter', 45)] assert [['Linda', 25], ['Daniel', 35]] == people.stream() .filter(p -> p.age < 40) .sorted((p1, p2) -> p1.age <=> p2.age) .map(p -> [p.name, p.age]) .toList() • Map, filter, reduce style using lambdas/streams
  53. Language Integrated Query record Person(String name, int age) {} def

    people = [new Person('Daniel', 35), new Person('Linda', 25), new Person('Peter', 45)] assert [['Linda', 25], ['Daniel', 35]] == GQ { from p in people where p.age < 40 orderby p.age select p.name, p.age }.toList()
  54. GINQ

  55. Language Integrated Query: JSON example import groovy.json.JsonSlurper def json =

    new JsonSlurper().parseText(''' { "prices": [ {"name": "Kakuda plum", "price": 13}, {"name": "Camu camu", "price": 25}, {"name": "Acerola cherries", "price": 39}, {"name": "Guava", "price": 2.5}, {"name": "Kiwifruit", "price": 0.4}, {"name": "Orange", "price": 0.4} ], "vitC": [ {"name": "Kakuda plum", "conc": 5300}, {"name": "Camu camu", "conc": 2800}, {"name": "Acerola cherries", "conc": 1677}, {"name": "Guava", "conc": 228}, {"name": "Kiwifruit", "conc": 144}, {"name": "Orange", "conc": 53} ] } ''')
  56. Language Integrated Query: JSON example import groovy.json.JsonSlurper def json =

    new JsonSlurper().parseText(''' { "prices": [ {"name": "Kakuda plum", "price": 13}, {"name": "Camu camu", "price": 25}, {"name": "Acerola cherries", "price": 39}, {"name": "Guava", "price": 2.5}, {"name": "Kiwifruit", "price": 0.4}, {"name": "Orange", "price": 0.4} ], "vitC": [ {"name": "Kakuda plum", "conc": 5300}, {"name": "Camu camu", "conc": 2800}, {"name": "Acerola cherries", "conc": 1677}, {"name": "Guava", "conc": 228}, {"name": "Kiwifruit", "conc": 144}, {"name": "Orange", "conc": 53} ] } ''') assert GQ { from p in json.prices join c in json.vitC on c.name == p.name orderby c.conc / p.price in desc limit 2 select p.name }.toList() == ['Kakuda plum', 'Kiwifruit']
  57. Language Integrated Query: XML example import groovy.xml.XmlSlurper def root =

    new XmlSlurper().parseText(''' <root> <prices> <price name="Kakuda plum">13</price> <price name="Camu camu">25</price> <price name="Acerola cherries">39</price> <price name="Guava">2.5</price> <price name="Kiwifruit">0.4</price> <price name="Orange">0.4</price> </prices> <vitaminC> <conc name="Kakuda plum">5300</conc> <conc name="Camu camuum">2800</conc> <conc name="Acerola cherries">1677</conc> <conc name="Guava">228</conc> <conc name="Kiwifruit">144</conc> <conc name="Orange">53</conc> </vitaminC> </root> ''')
  58. Language Integrated Query: XML example import groovy.xml.XmlSlurper def root =

    new XmlSlurper().parseText(''' <root> <prices> <price name="Kakuda plum">13</price> <price name="Camu camu">25</price> <price name="Acerola cherries">39</price> <price name="Guava">2.5</price> <price name="Kiwifruit">0.4</price> <price name="Orange">0.4</price> </prices> <vitaminC> <conc name="Kakuda plum">5300</conc> <conc name="Camu camuum">2800</conc> <conc name="Acerola cherries">1677</conc> <conc name="Guava">228</conc> <conc name="Kiwifruit">144</conc> <conc name="Orange">53</conc> </vitaminC> </root> ''') assert GQ { from p in root.prices.price join c in root.vitaminC.conc on c.@name == p.@name orderby c.toInteger() / p.toDouble() in desc limit 2 select p.@name }.toList() == ['Kakuda plum', 'Kiwifruit']
  59. Language Integrated Query: SQL example // ... create sql connection

    ... def price = sql.rows('SELECT * FROM Price') def vitC = sql.rows('SELECT * FROM VitaminC') assert GQ { from p in price join c in vitC on c.name == p.name orderby c.per100g / p.per100g in desc limit 2 select p.name }.toList() == ['Kakuda plum', 'Kiwifruit'] // ... close connection ...
  60. Type Annotations • Existing support • Now supported @Grab('net.jqwik:jqwik:1.6.5') import

    net.jqwik.api.* import net.jqwik.api.constraints.* class PropertyBasedTests { @Property def uniqueInList(@ForAll @Size(5) @UniqueElements List<@IntRange(min = 0, max = 10) Integer> aList) { assert aList.size() == aList.toSet().size() assert aList.every{ anInt -> anInt >= 0 && anInt <= 10 } } }
  61. Type Annotations @Grab('org.hibernate.validator:hibernate-validator:7.0.1.Final') @Grab('org.hibernate.validator:hibernate-validator-cdi:7.0.1.Final') @Grab('org.glassfish:jakarta.el:4.0.0') import jakarta.validation.constraints.* import jakarta.validation.* import

    groovy.transform.* @Canonical class Car { @NotNull @Size(min = 2, max = 14) String make @Min(1L) int seats List<@NotBlank String> owners } def validator = Validation.buildDefaultValidatorFactory().validator def violations = validator.validate(new Car(make: 'T', seats: 1)) assert violations*.message == ['size must be between 2 and 14'] violations = validator.validate(new Car(make: 'Tesla', owners: [''])) assert violations*.message.toSet() == ['must be greater than or equal to 1', 'must not be blank'] as Set violations = validator.validate(new Car(make: 'Tesla', owners: ['Elon'], seats: 2)) assert !violations
  62. Consolidation & Structuring • Maven coordinates • Module changes •

    Indy only, Parrot only • ~33% smaller zip • ~10% smaller core jar Language Features • Switch expressions • Records & Sealed types • Language integrated query • Improved type annotations Libraries/Tooling • Built-in type checkers • Built-in macro methods • TOML builder/slurper • JavaShell • Improved ranges AST transforms • @POJO • @RecordType • Groovy Contracts GDK enhancements Groovy 4 - Summary
  63. Built-in type checkers: regex checker def newYearsEve = '2020-12-31' def

    matcher = newYearsEve =~ /(\d{4})-(\d{1,2})-(\d{1,2}/ // ???
  64. Built-in type checkers: regex checker def newYearsEve = '2020-12-31' def

    matcher = newYearsEve =~ /(\d{4})-(\d{1,2})-(\d{1,2}/ // PatternSyntaxException
  65. Built-in type checkers: regex checker import groovy.transform.TypeChecked @TypeChecked(extensions = 'groovy.typecheckers.RegexChecker')

    def whenIs2020Over() { def newYearsEve = '2020-12-31' def matcher = newYearsEve =~ /(\d{4})-(\d{1,2})-(\d{1,2}/ } def newYearsEve = '2020-12-31' def matcher = newYearsEve =~ /(\d{4})-(\d{1,2})-(\d{1,2}/ // PatternSyntaxException
  66. Built-in type checkers: regex checker ~/\w{3/ // missing closing repetition

    quantifier brace ~"(.)o(.*" // missing closing group bracket Pattern.compile(/?/) // dangling meta character '?' (Java longhand) 'foobar' =~ /f[o]{2/ // missing closing repetition quantifier brace 'foobar' ==~ /(foo/ // missing closing group bracket Pattern.matches(/?/, 'foo') // dangling meta character '?' (Java longhand) def m = 'foobar' =~ /(...)(...)/ assert m[0][1] == 'foo' // okay assert m[0][3] // type error: only two groups in regex Pattern p = Pattern.compile('(...)(...)') Matcher m = p.matcher('foobar') assert m.find() assert m.group(1) == 'foo' // okay assert m.group(3) // type error: only two groups in regex
  67. Built-in macro methods: SV, SVI, SVD, NV, NVL def num

    = 42 def list = [1 ,2, 3] def range = 0..5 def string = 'foo' println SV(num, list, range, string) println SVI(range) println SVD(range) num=42, list=[1, 2, 3], range=[0, 1, 2, 3, 4, 5], string=foo range=0..5 range=<groovy.lang.IntRange@14 from=0 to=5 reverse=false inclusiveRight=true inclusiveLeft=true def r = NV(range) assert r instanceof NamedValue assert r.name == 'range' && r.val == 0..5
  68. TOML Builder/Slurper Incubating def ts = new TomlSlurper() def toml

    = ts.parseText(builder.toString()) assert 'HSV Maloo' == toml.records.car.name assert 'Holden' == toml.records.car.make assert 2006 == toml.records.car.year assert 'Australia' == toml.records.car.country assert 'http://example.org' == toml.records.car.homepage assert 'speed' == toml.records.car.record.type assert 'production pickup truck 271kph' == toml.records.car.record.description def builder = new TomlBuilder() builder.records { car { name 'HSV Maloo' make 'Holden' year 2006 country 'Australia' homepage new URL('http://example.org') record { type 'speed' description 'production pickup truck 271kph' } } }
  69. JavaShell import org.apache.groovy.util.JavaShell def opts = ['--enable-preview', '--release', '14'] def

    src = 'record Coord(int x, int y) {}' Class coordClass = new JavaShell().compile('Coord', opts, src) assert coordClass.newInstance(5, 10).toString() == 'Coord[x=5, y=10]'
  70. Improved Ranges def range = 1..5 assert range == [1,

    2, 3, 4, 5] range = 1..<5 assert range == [1, 2, 3, 4] range = 1<..5 assert range == [2, 3, 4, 5] range = 1<..<5 assert range == [2, 3, 4] • Existing support • Now supported
  71. Consolidation & Structuring • Maven coordinates • Module changes •

    Indy only, Parrot only • ~33% smaller zip • ~10% smaller core jar Language Features • Switch expressions • Records & Sealed types • Language integrated query • Improved type annotations Libraries/Tooling • Built-in type checkers • Built-in macro methods • TOML builder/slurper • JavaShell • Improved ranges AST transforms • @POJO • @RecordType • Groovy Contracts GDK enhancements Groovy 4 - Summary
  72. AST Transformations class Book { List<String> authors String title Date

    publicationDate }
  73. AST Transformations class Book { List<String> authors String title Date

    publicationDate } public class Book implements GroovyObject { private java.util.List<String> authors private java.lang.String title private java.util.Date publicationDate public java.util.List<String> getAuthors() { ... } public void setAuthors(java.util.List<String> value) { ... } public java.lang.String getTitle() { ... } public void setTitle(java.lang.String value) { ... } public java.util.Date getPublicationDate() { ... } public void setPublicationDate(java.util.Date value) { ... } }
  74. AST Transformations @ToString class Book { List<String> authors String title

    Date publicationDate }
  75. AST Transformations @ToString class Book { List<String> authors String title

    Date publicationDate } public class Book implements GroovyObject { private java.util.List<String> authors private java.lang.String title private java.util.Date publicationDate public java.util.List<String> getAuthors() { ... } public void setAuthors(java.util.List<String> value) { ... } public java.lang.String getTitle() { ... } public void setTitle(java.lang.String value) { ... } public java.util.Date getPublicationDate() { ... } public void setPublicationDate(java.util.Date value) { ... } public java.lang.String toString() { /* build toString based on properties */ } }
  76. AST Transformations @Immutable(copyWith = true) @Sortable(excludes = 'authors') @AutoExternalize class

    Book { @IndexedProperty List<String> authors String title Date publicationDate }
  77. AST Transformations // imports not shown public class Book {

    private String $to$string; private int $hash$code; private final List<String> authors; private final String title; private final Date publicationDate; private static final java.util.Comparator this$TitleComparator; private static final java.util.Comparator this$PublicationDateComparator; public Book(List<String> authors, String title, Date publicationDate) { if (authors == null) { this.authors = null; } else { if (authors instanceof Cloneable) { List<String> authorsCopy = (List<String>) ((ArrayList<?>) authors).clone(); this.authors = (List<String>) (authorsCopy instanceof SortedSet ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof SortedMap ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof Set ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof Map ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof List ? DefaultGroovyMethods.asImmutable(authorsCopy) : DefaultGroovyMethods.asImmutable(authorsCopy)); } else { this.authors = (List<String>) (authors instanceof SortedSet ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof SortedMap ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof Set ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof Map ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof List ? DefaultGroovyMethods.asImmutable(authors) : DefaultGroovyMethods.asImmutable(authors)); } } this.title= title; if (publicationDate== null) { this.publicationDate= null; } else { this.publicationDate= (Date) publicationDate.clone(); } } public Book(Map args) { if ( args == null) { args = new HashMap(); } ImmutableASTTransformation.checkPropNames(this, args); if (args.containsKey("authors")) { if ( args.get("authors") == null) { this .authors = null; } else { if (args.get("authors") instanceof Cloneable) { List<String> authorsCopy = (List<String>) ((ArrayList<?>) args.get("authors")).clone(); this.authors = (List<String>) (authorsCopy instanceof SortedSet ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof SortedMap ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof Set ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof Map ? DefaultGroovyMethods.asImmutable(authorsCopy) : authorsCopy instanceof List ? DefaultGroovyMethods.asImmutable(authorsCopy) : DefaultGroovyMethods.asImmutable(authorsCopy)); } else { List<String> authors = (List<String>) args.get("authors"); this.authors = (List<String>) (authors instanceof SortedSet ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof SortedMap ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof Set ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof Map ? DefaultGroovyMethods.asImmutable(authors) : authors instanceof List ? DefaultGroovyMethods.asImmutable(authors) : DefaultGroovyMethods.asImmutable(authors)); } } } else { this .authors = null; } if (args.containsKey("title")) {this .title = (String) args.get("title"); } else { this .title = null;} if (args.containsKey("publicationDate")) { if (args.get("publicationDate") == null) { this.publicationDate = null; } else { this.publicationDate = (Date) ((Date) args.get("publicationDate")).clone(); } } else {this.publicationDate = null; } } … public Book() { this (new HashMap()); } public int compareTo(Book other) { if (this == other) { return 0; } Integer value = 0 value = this .title <=> other .title if ( value != 0) { return value } value = this .publicationDate <=> other .publicationDate if ( value != 0) { return value } return 0 } public static Comparator comparatorByTitle() { return this$TitleComparator; } public static Comparator comparatorByPublicationDate() { return this$PublicationDateComparator; } public String toString() { StringBuilder _result = new StringBuilder(); boolean $toStringFirst= true; _result.append("Book("); if ($toStringFirst) { $toStringFirst = false; } else { _result.append(", "); } _result.append(InvokerHelper.toString(this.getAuthors())); if ($toStringFirst) { $toStringFirst = false; } else { _result.append(", "); } _result.append(InvokerHelper.toString(this.getTitle())); if ($toStringFirst) { $toStringFirst = false; } else { _result.append(", "); } _result.append(InvokerHelper.toString(this.getPublicationDate())); _result.append(")"); if ($to$string == null) { $to$string = _result.toString(); } return $to$string; } public int hashCode() { if ( $hash$code == 0) { int _result = HashCodeHelper.initHash(); if (!(this.getAuthors().equals(this))) { _result = HashCodeHelper.updateHash(_result, this.getAuthors()); } if (!(this.getTitle().equals(this))) { _result = HashCodeHelper.updateHash(_result, this.getTitle()); } if (!(this.getPublicationDate().equals(this))) { _result = HashCodeHelper.updateHash(_result, this.getPublicationDate()); } $hash$code = (int) _result; } return $hash$code; } public boolean canEqual(Object other) { return other instanceof Book; } … public boolean equals(Object other) { if ( other == null) { return false; } if (this == other) { return true; } if (!( other instanceof Book)) { return false; } Book otherTyped = (Book) other; if (!(otherTyped.canEqual( this ))) { return false; } if (!(this.getAuthors() == otherTyped.getAuthors())) { return false; } if (!(this.getTitle().equals(otherTyped.getTitle()))) { return false; } if (!(this.getPublicationDate().equals(otherTyped.getPublicationDate()))) { return false; } return true; } public final Book copyWith(Map map) { if (map == null || map.size() == 0) { return this; } Boolean dirty = false; HashMap construct = new HashMap(); if (map.containsKey("authors")) { Object newValue = map.get("authors"); Object oldValue = this.getAuthors(); if (newValue != oldValue) { oldValue= newValue; dirty = true; } construct.put("authors", oldValue); } else { construct.put("authors", this.getAuthors()); } if (map.containsKey("title")) { Object newValue = map.get("title"); Object oldValue = this.getTitle(); if (newValue != oldValue) { oldValue= newValue; dirty = true; } construct.put("title", oldValue); } else { construct.put("title", this.getTitle()); } if (map.containsKey("publicationDate")) { Object newValue = map.get("publicationDate"); Object oldValue = this.getPublicationDate(); if (newValue != oldValue) { oldValue= newValue; dirty = true; } construct.put("publicationDate", oldValue); } else { construct.put("publicationDate", this.getPublicationDate()); } return dirty == true ? new Book(construct) : this; } public void writeExternal(ObjectOutputout) throws IOException { out.writeObject(authors); out.writeObject(title); out.writeObject(publicationDate); } public void readExternal(ObjectInputoin) throws IOException, ClassNotFoundException{ authors = (List) oin.readObject(); title = (String) oin.readObject(); publicationDate= (Date) oin.readObject(); } … static { this$TitleComparator = new Book$TitleComparator(); this$PublicationDateComparator = new Book$PublicationDateComparator(); } public String getAuthors(int index) { return authors.get(index); } public List<String> getAuthors() { return authors; } public final String getTitle() { return title; } public final Date getPublicationDate() { if (publicationDate== null) { return publicationDate; } else { return (Date) publicationDate.clone(); } } public int compare(java.lang.Objectparam0, java.lang.Objectparam1) { return -1; } private static class Book$TitleComparator extends AbstractComparator<Book> { public Book$TitleComparator() { } public int compare(Book arg0, Book arg1) { if (arg0 == arg1) { return 0; } if (arg0 != null && arg1 == null) { return -1; } if (arg0 == null && arg1 != null) { return 1; } return arg0.title <=> arg1.title; } public int compare(java.lang.Objectparam0, java.lang.Objectparam1) { return -1; } } private static class Book$PublicationDateComparator extends AbstractComparator<Book> { public Book$PublicationDateComparator() { } public int compare(Book arg0, Book arg1) { if ( arg0 == arg1 ) { return 0; } if ( arg0 != null && arg1 == null) { return -1; } if ( arg0 == null && arg1 != null) { return 1; } return arg0 .publicationDate <=> arg1 .publicationDate; } public int compare(java.lang.Objectparam0, java.lang.Objectparam1) { return -1; } } } @Immutable(copyWith = true) @Sortable(excludes = 'authors') @AutoExternalize class Book { @IndexedProperty List<String> authors String title Date publicationDate }
  78. AST Transformations: Groovy 2.4, Groovy 2.5, Groovy 3.0, Groovy 4.0*

    * Now also priorities for AST transforms in the same phase
  79. AST Transformations: Groovy 2.4, Groovy 2.5, Groovy 3.0, Groovy 4.0

    @NonSealed @RecordBase @Sealed @PlatformLog @GQ @Final @RecordType @POJO @Pure @Contracted @Ensures @Invariant @Requires @ClassInvariant @ContractElement @Postcondition @Precondition (Improved in 2.5)
  80. Groovy 2.5: AST Transforms: @Immutable becomes meta-annotation @Immutable class Point

    { int x, y } @ToString(includeSuperProperties = true, cache = true) @EqualsAndHashCode(cache = true) @ImmutableBase @ImmutableOptions @PropertyOptions(propertyHandler = ImmutablePropertyHandler) @TupleConstructor(defaults = false) @MapConstructor(noArg = true, includeSuperProperties = true, includeFields = true) @KnownImmutable class Point { int x, y }
  81. Records revisited (@RecordType meta-annotation) record Point(int x, y){ } @RecordBase

    // RecordBase makes implicit changes equivalent to native // toString(), equals() and hashCode() record implements or: // @ToString(cache = false, includeNames = true, includePackage = false, // leftDelimiter = '[', rightDelimiter = ']', pojo = true, // nameValueSeparator = '=', fieldSeparator = ", ") // @EqualsAndHashCode(useCanEqual = false, pojo = true) @RecordOptions @TupleConstructor(namedVariant = true, force = true, defaultsMode = AUTO) @PropertyOptions @KnownImmutable @POJO @CompileStatic class Point { int x, y } @RecordType class Point { int x, y }
  82. Declarative Record Customization def a = new Agenda(topics: ['Sealed', 'Records'])

    assert a.topics().size() == 2 assert a.toString() == 'Agenda[topics=[Sealed, Records]]' a.topics().clear() a.topics() << 'Switch Expressions' assert a.topics().size() == 1 record Agenda(List topics) { }
  83. Declarative Record Customization def a = new Agenda(topics: ['Sealed', 'Records'])

    assert a.topics().size() == 2 shouldFail(UnsupportedOperationException) { a.topics().clear() } assert a.toString() == 'Agenda([Sealed, Records])' @ToString @PropertyOptions(propertyHandler = ImmutablePropertyHandler) record Agenda(List topics) { }
  84. @POJO 3 true Point(x:1, y:3) Point(x:2, y:2) Point(x:3, y:1) 2

    Incubating
  85. groovy-contracts module Design-by-contract import groovy.contracts.* @Invariant({ speed() >= 0 })

    class Rocket { int speed = 0 boolean started = true @Requires({ isStarted() }) @Ensures({ old.speed < speed }) def accelerate(inc) { speed += inc } def isStarted() { started } def speed() { speed } } def r = new Rocket() r.accelerate(5)
  86. groovy-contracts module Design-by-contract abstract class Material { abstract String getColor()

    @Requires({ color != null }) String toString() { "${getClass().name} with color ${color.toLowerCase()}" } } new Glass().toString() // PreconditionViolation • Alternative way to harden earlier sealed example
  87. Consolidation & Structuring • Maven coordinates • Module changes •

    Indy only, Parrot only • ~33% smaller zip • ~10% smaller core jar Language Features • Switch expressions • Records & Sealed types • Language integrated query • Improved type annotations Libraries/Tooling • Built-in type checkers • Built-in macro methods • TOML builder/slurper • JavaShell • Improved ranges AST transforms • @POJO • @RecordType • Groovy Contracts GDK enhancements Groovy 4 - Summary
  88. GDK Enhancements assert (Stream.of(1) + Stream.of(2)).toList() == [1,2] println Runtime.runtime.pid

  89. On-going research for future Groovy versions • Additional switch destructuring/pattern

    matching • instanceof “pattern matching” • Smarter type checking: non-null, pure • Module definitions in Groovy • Syntactic sugar wrapper for JDK11 HttpClient
  90. Join us: groovy.apache.org