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.

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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  7. Important naming/structuring changes
    Maven coordinate change
    org.codehaus.groovy org.apache.groovy

    View Slide

  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.

    View Slide

  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

    View Slide

  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

    View Slide

  11. Legacy consolidation
    Old parser removal
    Antlr 2 Antlr4
    Classic bytecode generation removal
    Classic Indy

    View Slide

  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

    View Slide

  13. Switch expressions
    def a = 9
    def result = switch(a) {
    case 6, 8 -> 'b'
    case 9 -> 'c'
    default -> 'z'
    }
    assert 'c' == result

    View Slide

  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]

    View Slide

  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]

    View Slide

  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  

    View Slide

  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

    View Slide

  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) { }

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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)

    View Slide

  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)
      

    View Slide

  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+

    View Slide

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

    View Slide

  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)

    View Slide

  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

    View Slide

  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]

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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
    }

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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)

    View Slide

  44. Sealed Types – Good for ADTs
    @Sealed interface Tree {}
    @Singleton final class Empty implements Tree {
    String toString() { 'Empty' }
    }
    @Canonical final class Node implements Tree {
    T value
    Tree left, right
    }
    Tree tree = new Node<>(42,
    new Node<>(0, Empty.instance, Empty.instance), Empty.instance)
    assert tree.toString() == 'Node(42, Node(0, Empty, Empty), Empty)'

    View Slide

  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 { }

    View Slide

  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

    View Slide

  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 { }

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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()

    View Slide

  54. GINQ

    View Slide

  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}
    ]
    }
    ''')

    View Slide

  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']

    View Slide

  57. Language Integrated Query: XML example
    import groovy.xml.XmlSlurper
    def root = new XmlSlurper().parseText('''


    13
    25
    39
    2.5
    0.4
    0.4


    5300
    2800
    1677
    228
    144
    53


    ''')

    View Slide

  58. Language Integrated Query: XML example
    import groovy.xml.XmlSlurper
    def root = new XmlSlurper().parseText('''


    13
    25
    39
    2.5
    0.4
    0.4


    5300
    2800
    1677
    228
    144
    53


    ''')
    assert GQ {
    from p in root.prices.price
    join c in root.vitaminC.conc on [email protected] == [email protected]
    orderby c.toInteger() / p.toDouble() in desc
    limit 2
    select [email protected]
    }.toList() == ['Kakuda plum', 'Kiwifruit']

    View Slide

  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 ...

    View Slide

  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 aList) {
    assert aList.size() == aList.toSet().size()
    assert aList.every{ anInt -> anInt >= 0 && anInt <= 10 }
    }
    }

    View Slide

  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 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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  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=inclusiveRight=true inclusiveLeft=true
    def r = NV(range)
    assert r instanceof NamedValue
    assert r.name == 'range' && r.val == 0..5

    View Slide

  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'
    }
    }
    }

    View Slide

  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]'

    View Slide

  70. Improved Ranges
    def range = 1..5
    assert range == [1, 2, 3, 4, 5]
    range = 1..<5
    assert range == [1, 2, 3, 4]
    range = 1assert range == [2, 3, 4, 5]
    range = 1assert range == [2, 3, 4]
    • Existing
    support
    • Now
    supported

    View Slide

  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

    View Slide

  72. AST Transformations
    class Book {
    List authors
    String title
    Date publicationDate
    }

    View Slide

  73. AST Transformations
    class Book {
    List authors
    String title
    Date publicationDate
    }
    public class Book implements GroovyObject {
    private java.util.List authors
    private java.lang.String title
    private java.util.Date publicationDate
    public java.util.List getAuthors() { ... }
    public void setAuthors(java.util.List 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) { ... }
    }

    View Slide

  74. AST Transformations
    @ToString
    class Book {
    List authors
    String title
    Date publicationDate
    }

    View Slide

  75. AST Transformations
    @ToString
    class Book {
    List authors
    String title
    Date publicationDate
    }
    public class Book implements GroovyObject {
    private java.util.List authors
    private java.lang.String title
    private java.util.Date publicationDate
    public java.util.List getAuthors() { ... }
    public void setAuthors(java.util.List 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 */
    }
    }

    View Slide

  76. AST Transformations
    @Immutable(copyWith = true)
    @Sortable(excludes = 'authors')
    @AutoExternalize
    class Book {
    @IndexedProperty
    List authors
    String title
    Date publicationDate
    }

    View Slide

  77. AST Transformations
    // imports not shown
    public class Book {
    private String $to$string;
    private int $hash$code;
    private final List 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 authors, String title, Date publicationDate) {
    if (authors == null) {
    this.authors = null;
    } else {
    if (authors instanceof Cloneable) {
    List authorsCopy = (List) ((ArrayList>) authors).clone();
    this.authors = (List) (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) (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 authorsCopy = (List) ((ArrayList>) args.get("authors")).clone();
    this.authors = (List) (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 authors = (List) args.get("authors");
    this.authors = (List) (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 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 {
    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 {
    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 authors
    String title
    Date publicationDate
    }

    View Slide

  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

    View Slide

  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)

    View Slide

  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
    }

    View Slide

  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
    }

    View Slide

  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) { }

    View Slide

  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) { }

    View Slide

  84. @POJO
    3
    true
    Point(x:1, y:3)
    Point(x:2, y:2)
    Point(x:3, y:1)
    2
    Incubating

    View Slide

  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)

    View Slide

  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

    View Slide

  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

    View Slide

  88. GDK Enhancements
    assert (Stream.of(1) + Stream.of(2)).toList() == [1,2]
    println Runtime.runtime.pid

    View Slide

  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

    View Slide

  90. Join us:
    groovy.apache.org

    View Slide