This is an automated email from the ASF dual-hosted git repository.

paulk-asert pushed a commit to branch asf-site
in repository https://gitbox.apache.org/repos/asf/groovy-website.git


The following commit(s) were added to refs/heads/asf-site by this push:
     new e5ed3fb  draft blog post: groovy 6 and functional programming
e5ed3fb is described below

commit e5ed3fb205d8f6acded3de245585ae19b4b42dae
Author: Paul King <[email protected]>
AuthorDate: Wed May 20 14:12:00 2026 +1000

    draft blog post: groovy 6 and functional programming
---
 ...roovy6-features-for-functional-programmers.adoc | 594 +++++++++++++++++++++
 1 file changed, 594 insertions(+)

diff --git 
a/site/src/site/blog/groovy6-features-for-functional-programmers.adoc 
b/site/src/site/blog/groovy6-features-for-functional-programmers.adoc
new file mode 100644
index 0000000..31c461d
--- /dev/null
+++ b/site/src/site/blog/groovy6-features-for-functional-programmers.adoc
@@ -0,0 +1,594 @@
+= Groovy 6 features for Functional Programmers
+Paul King <paulk-asert|PMC_Member>
+:revdate: 2026-05-20T08:00:00+00:00
+:updated: 2026-05-20T08:00:00+00:00
+:draft: true
+:keywords: functional programming, monoid, semigroup, monad, purity, type 
checker, property-based testing
+:description: Groovy 6's stance on functional programming — same starting 
point as Java, but with checked algebra (Monoid/Semigroup), verified purity, 
monadic comprehensions, and machine-actionable annotations.
+
+== Introduction
+
+Groovy is not Haskell. It is not Scala. It does not have higher-kinded
+types, a `Functor`/`Applicative`/`Monad` hierarchy, or a totality checker
+in the compiler. Its starting point is Java's — a JVM language with
+closures, immutable collections and `java.util.stream.Stream`.
+
+What Groovy 6 _does_ do is close a handful of the gaps that send
+functional programmers reaching for FunctionalJava or highj when they
+work on the JVM. The new pieces fit together:
+
+* `@Associative`/`@Reducer` annotations that put a *monoid* contract
+on a binary method, verified at compile time by `CombinerChecker`.
+* `@Pure`/`@Modifies` annotations that let the compiler tell you a
+method has no side effects (or only the ones you allow) — a
+specification-driven alternative to lifting effects into an `IO` monad.
+* A `DO` macro (GEP-23) giving Scala-`for` / Haskell-`do` notation
+across `Optional`, `Stream`, `CompletableFuture`, Groovy's `Awaitable`,
+and any user type that opts in.
+* `NullChecker` — including a flow-sensitive `strict` mode requiring
+no annotations — for compile-time `Maybe` without the wrapper.
+* `val`, nested `copyWith` and richer destructuring on top of records
+and `@Immutable`, for the everyday immutable-update shapes that
+otherwise need a lens library.
+* `@Decreases` and `@Invariant` on loops, providing termination
+measures and loop invariants of the kind a totality checker would
+require.
+
+None of that turns Groovy into a pure-functional language. It does mean
+the parts of the FP toolkit that pay their way on a real JVM project —
+algebraic laws on combiners, declared purity, monadic value
+composition, exhaustive null handling, immutable updates — are now
+first-class.
+
+A companion project at
+https://github.com/paulk-asert/groovy6-functional[`groovy6-functional`]
+holds runnable versions of every example in this post.
+
+== What Groovy already gave you
+
+Groovy has been a usable language for functional programmers since the
+2.x line. Five things were already in the toolbox:
+
+. *First-class closures and function composition.* Closures are
+values; `<<` and `>>` compose them; `curry` and `rcurry` partially
+apply.
++
+[source,groovy]
+----
+var trim     = { String s -> s.trim() }
+var upper    = String::toUpperCase
+var sizeSq   = (String::size) >> { it ** 2 }
+var composed = sizeSq << (trim >> upper)
+assert composed(' foo bar ') == 49
+----
+
+. *Memoization and trampolining.*
+`closure.memoize()`, `memoizeAtMost(n)`, and `Closure.trampoline()`
+turn naive recursive definitions into something fit for production.
+
+. *Records, `@Immutable`, deep-immutable lists/maps via `.asImmutable()`.*
+Value semantics without ceremony.
+
+. *Tail-recursive methods* via `@TailRecursive` — the
+transform rewrites the body to a loop. No stack growth for `factorial`,
+`mutualOddEven`, list folds.
+
+. *Lazy streams and GINQ.* `Stream`, lazy iterators, and GINQ
+(a comprehension over collections and SQL-like sources) cover the
+lazy/declarative end of the lane.
+
+That toolkit was good for the workaday side of FP. It said little about
+the parts that distinguish FP-with-checking from FP-with-conventions:
+algebraic laws, declared effects, monadic composition for non-stream
+carriers, and ironclad immutability. Groovy 6 is mostly about those.
+
+== Monoids and Semigroups, checked
+
+A *monoid* is an associative binary operation with an identity. In
+Haskell:
+
+[source,haskell]
+----
+class Semigroup a where (<>)   :: a -> a -> a
+class Semigroup a => Monoid a where mempty :: a
+----
+
+In FunctionalJava you build a `Monoid<A>` by passing a combiner closure
+and a zero. In highj you import the typeclass instance for `Monoid<µ>`.
+In Groovy 6 the algebra moves _onto the method_:
+
+[source,groovy]
+----
+import groovy.transform.Associative
+import groovy.transform.Reducer
+
+class Sum {
+    @Associative @Reducer(zero = '0')
+    static int add(int a, int b) { a + b }
+}
+
+class Concat {
+    @Associative @Reducer(zero = '""')
+    static String join(String a, String b) { a + b }
+}
+
+class Tally {
+    @Associative @Reducer(zero = '[:]')
+    static Map<String, Integer> merge(Map<String, Integer> a, Map<String, 
Integer> b) {
+        var out = new LinkedHashMap(a)
+        b.each { k, v -> out.merge(k, v) { x, y -> x + y } }
+        out
+    }
+}
+----
+
+The point is not the annotations. The point is the type-checking
+extension behind them:
+
+[source,groovy]
+----
+@TypeChecked(extensions = 'groovy.typecheckers.CombinerChecker')
+def reductions() {
+    assert (1..100).toList().injectParallel(0, Sum.&add) == 5050
+    assert ['a', 'b', 'c'].sumParallel(Concat.&join) == 'abc'
+
+    // REJECTED at compile time — subtraction is not associative:
+    // [1, 2, 3].injectParallel(0) { a, b -> a - b }
+}
+----
+
+`injectParallel` and `sumParallel` partition, reorder and recombine
+input. They are only correct when the combiner is associative — and
+that is the bug class FP people normally protect themselves from by
+encoding the structure as a `Monoid<A>` value. A non-associative
+combiner like the subtraction example is a _semantic_ error: it
+compiles cleanly in Java and in Groovy without the checker, and
+surfaces as a non-deterministic wrong answer at runtime. The checker
+gives you the same guarantee a typeclass constraint gives Scala or
+Haskell, but without lifting `add` into a wrapper and unlifting the
+result.
+
+The `Monoid`/`Semigroup` types from FunctionalJava and highj are also
+recognised, which means existing FJ-shaped combiners flow through
+`sumParallel` without rewriting.
+
+== Purity and frame conditions — Groovy's answer to `IO` / `State`
+
+The other classical FP move is to lift effectful work into the type
+system: `IO` for arbitrary effects, `State s` for threaded state,
+`Reader r` for environment access, `Writer w` for log accumulation.
+Groovy 6 takes a different route to the same outcome — verified
+declarations, no lift / unlift:
+
+[source,groovy]
+----
+@TypeChecked(extensions = ['groovy.typecheckers.PurityChecker',
+                           'groovy.typecheckers.ModifiesChecker'])
+class Calculator {
+    BigDecimal total = 0
+    List<String> ledger = []
+
+    @Pure
+    static BigDecimal vat(BigDecimal net, BigDecimal rate) {
+        net * (1 + rate)
+    }
+
+    @Requires({ amount > 0 })
+    @Ensures ({ total == old.total + amount })
+    @Modifies({ [this.total, this.ledger] })
+    void post(BigDecimal amount) {
+        total += amount
+        ledger << "+$amount"
+    }
+
+    @Pure(allows = Pure.Effect.LOGGING)
+    BigDecimal balance() {
+        log.fine "balance read"
+        total
+    }
+}
+----
+
+There are four FP-flavoured facts the compiler now knows about
+`Calculator`:
+
+* `vat` is pure — referentially transparent in the Haskell sense.
+`PurityChecker` rejects any side-effecting body.
+* `post` may only modify `total` and `ledger`. Touch any other field
+and `ModifiesChecker` errors. This is the `State` monad's job
+(_what_ state can change) without the bind boilerplate.
+* `@Requires`/`@Ensures` are the Hoare-logic dual of `State`'s
+state-transition function — pre- and post-conditions, with `old.`
+referring to pre-state.
+* `balance` is pure _modulo logging_. The `allows` set is a small
+effect lattice (`LOGGING`, `METRICS`, `IO`, `NONDETERMINISM`) —
+graded effects, in the cats-effect sense, but the verification work
+is in the checker rather than the type.
+
+For an FP audience the unfamiliar half is that none of these methods
+return a wrapped value. The familiar half is that the compiler knows
+what each method may and may not do, and a reader (or an AI agent) can
+work from the signature alone.
+
+== Monadic comprehensions: `DO`
+
+GEP-23 introduces `DO` — a comprehension macro that desugars to the
+carrier's `flatMap`/`map` (or `thenCompose`/`thenApply`, or whatever
+the carrier's standard pair is). The notation is Scala-`for` /
+Haskell-`do` shaped:
+
+[source,groovy]
+----
+import static org.apache.groovy.macrolib.MacroLibGroovyMethods.DO
+
+@TypeChecked(extensions = 'groovy.typecheckers.MonadicChecker')
+Optional<String> greet(Map<String, String> users, String userId) {
+    DO(user in Optional.ofNullable(users[userId]),
+       name in Optional.ofNullable(user.split(/\|/)[0])) {
+        Optional.of("Hello, $name!".toString())
+    }
+}
+
+assert greet([u42: 'Alice|admin'], 'u42').get() == 'Hello, Alice!'
+assert greet([u42: 'Alice|admin'], 'u99') == Optional.empty()
+----
+
+The same shape works over `Awaitable` (so the dependency graph in an
+async computation is in the source rather than spread across
+`thenCompose` calls):
+
+[source,groovy]
+----
+@TypeChecked(extensions = 'groovy.typecheckers.MonadicChecker')
+Awaitable<String> greetByKey(String key) {
+    DO(id   in fetchId(key),
+       name in fetchName(id),
+       msg  in greeting(name)) {
+        async { msg }
+    }
+}
+----
+
+And over FunctionalJava's `Validation` — no annotation, no wrapper, no
+Groovy dependency on FunctionalJava, because `fj.data.Validation` is
+recognised by name in `DO`'s standard allow-list:
+
+[source,groovy]
+----
+@TypeChecked(extensions = 'groovy.typecheckers.MonadicChecker')
+Validation<String, Integer> add(String a, String b) {
+    DO(x in parsePositive(a),
+       y in parsePositive(b)) {
+        Validation.success(x + y)
+    }
+}
+assert add('2', '3').successE() == 5
+assert add('hi', '3').failE()   == 'not numeric: hi'
+----
+
+What `DO` does *not* do: it does not introduce higher-kinded types, it
+does not mix carriers in one comprehension (nest for that), and it does
+not synthesise a `pure`/`return` — the body must explicitly yield a
+carrier value. That is the deliberate non-goal list in
+link:../wiki/GEP-23.html[GEP-23].
+
+== Linting native chains: `MonadicShapeChecker`
+
+For codebases that prefer `flatMap`/`map` chains over comprehensions,
+`MonadicShapeChecker` lints the chain against the same registry that
+`DO` uses. It catches three classic foot-guns:
+
+[cols="3,2",options="header"]
+|===
+| Mistake                                       | Checker says
+
+| `Optional.of(1).flatMap { it + 1 }`           | `flatMap` must return a 
carrier
+| `Stream.of(1).flatMap { Optional.of(it) }`    | bind must return the _same_ 
carrier
+| `Optional.of(1).map { Optional.of(it) }`      | `M<M<T>>` — did you mean 
`flatMap`?
+|===
+
+The first two would be compile errors in Java, but Groovy's
+single-abstract-method coercion of closures normally lets them through —
+the checker restores the Java guarantee. The third compiles cleanly in
+*both* Java and Groovy: an `Optional<Optional<Integer>>` is a perfectly
+well-typed value, just (almost certainly) not the one you intended. That
+is the `flatMap`/`map` mix-up the checker catches for everyone.
+
+== Compile-time `Maybe` without the wrapper
+
+In Haskell, `Maybe a` is a discipline you adopt: every value that
+might be absent is wrapped, the compiler tracks it, you handle the
+`Nothing` case at the leaf.
+
+In Groovy 6, `NullChecker` is the equivalent discipline _without_ the
+wrapper. You can drive it with `@Nullable`/`@NonNull` if you like:
+
+[source,groovy]
+----
+@TypeChecked(extensions = 'groovy.typecheckers.NullChecker')
+int safeLength(@Nullable String text) {
+    if (text != null) {
+        return text.length()         // ok — narrowed
+    }
+    -1
+}
+----
+
+…but a deliberate goal of the Groovy 6 approach is to _not_ make you
+litter code with annotations. Flow-sensitive `strict` mode does the
+same analysis on entirely unannotated code:
+
+[source,groovy]
+----
+@TypeChecked(extensions = 'groovy.typecheckers.NullChecker(strict: true)')
+def strictDemo() {
+    def x = null
+    // x.toString()                  // compile error: x may be null
+    x = 'hello'
+    assert x.toString() == 'hello'   // ok — reassigned
+}
+----
+
+That covers code under your control. The trickier case is the
+boundary: a third-party library that exposes possibly-null returns and
+never annotated them. Groovy 6 stacks a few mitigations here:
+
+* `@Nullable`/`@NonNull` are matched _by simple name from any package_
+— JSpecify, JSR-305, JetBrains, SpotBugs, Checker Framework, or your
+own marker — so whichever vendor's annotations the library author did
+or didn't pick, the checker sees them.
+* Where the library is annotation-free, contract annotations contribute
+nullability facts: a top-level `x != null` conjunct in `@Requires` marks
+the parameter as implicitly `@NonNull`, and `result != null` in
+`@Ensures` does the same for the return. You assert the boundary fact
+once at your wrapping method and the checker propagates it inward.
+* `@NullCheck` auto-generates null guards on parameters where you
+want fail-fast behaviour rather than tracked nullability.
+* `@MonotonicNonNull` covers the lazy-init case (read as nullable until
+first assignment, non-null afterwards).
+* Safe navigation (`?.`) and the Elvis operator (`?:`) — Groovy
+features that pre-date NullChecker — are recognised by the checker as
+narrowing forms, so the idiomatic `lib.maybe()?.size() ?: 0` typechecks
+without further annotation.
+
+The trade — versus `Optional<T>` everywhere — is the one Kotlin made:
+the analysis is on the variable, not in the type, and the cost at the
+call site is zero.
+
+== Immutability ergonomics
+
+Three Groovy 6 changes make the day-to-day immutable-update shapes
+match what you get from a Haskell record-update syntax or a Scala
+`copy` call:
+
+[source,groovy]
+----
+@Immutable(copyWith = true) class Address { String city, zip }
+@Immutable(copyWith = true) class Person  { String name; Address address }
+
+val alice = new Person('Alice', new Address('NYC', '10001'))
+
+val moved = alice.copyWith('address.city': 'Boston')
+assert moved.address.zip == '10001'         // structural sharing — untouched
+
+val swapped = alice.copyWith {
+    name = 'Alice2'
+    address.city = old.address.city.reverse()
+}
+
+val (name: who, age: _) = [name: 'Bob', age: 30]
+def (h, *t) = [1, 2, 3, 4]
+----
+
+`val` is the immutable-binding companion to `var`. Nested `copyWith`
+gives you lens-style updates without a lens library — structural
+sharing is preserved transitively, so `is`-identity holds on untouched
+branches. The destructuring extension reaches the "match the parts you
+care about" cases pattern matching is normally used for.
+
+== Termination and loop invariants
+
+Totality matters to FP people. `@TailRecursive` was already there;
+`@Decreases` and `@Invariant` on loops are new:
+
+[source,groovy]
+----
+@Ensures({ result.isSorted() })
+List merge(List in1, List in2) {
+    var out = []
+    var count = in1.size() + in2.size()
+    @Invariant({ in1.size() + in2.size() + out.size() == count })
+    @Decreases({ [in1.size(), in2.size()] })
+    while (in1 || in2) {
+        if (!in1) return out + in2
+        if (!in2) return out + in1
+        out += (in1[0] < in2[0]) ? in1.pop() : in2.pop()
+    }
+    out
+}
+----
+
+The `@Invariant` says nothing is lost or gained across iterations.
+The `@Decreases` gives a lexicographic termination measure: the pair
+`[in1.size(), in2.size()]` strictly decreases each round. That is the
+sort of obligation a totality checker in Idris or Agda would impose.
+The checker is contract-grade rather than proof-grade, but the
+declaration is the same shape.
+
+== Property-based tests, mechanically derived from the annotations
+
+The single biggest payoff of these annotations is that they are *not
+just for the compiler*. They are machine-readable, structurally
+identical across the codebase, and compiler-enforced — so an AI agent
+or a small ASM walker can use them as a specification:
+
+[source,markdown]
+----
+Prompt:
+  For every static method annotated @groovy.transform.Associative,
+  emit a jqwik @Property method asserting f(f(a,b), c) == f(a, f(b,c)).
+  If the method also carries @Reducer(zero = "<expr>"), emit a second
+  @Property asserting f(a, <expr>) == a and f(<expr>, a) == a.
+----
+
+Which produces, mechanically:
+
+[source,groovy]
+----
+class MonoidLawsTest {
+    @Property boolean addIsAssociative(@ForAll int a, @ForAll int b, @ForAll 
int c) {
+        Sum.add(Sum.add(a, b), c) == Sum.add(a, Sum.add(b, c))
+    }
+    @Property boolean addHasZero(@ForAll int a) {
+        Sum.add(a, 0) == a && Sum.add(0, a) == a
+    }
+    @Property boolean tallyIsAssociative(@ForAll Map<String, Integer> a,
+                                          @ForAll Map<String, Integer> b,
+                                          @ForAll Map<String, Integer> c) {
+        Tally.merge(Tally.merge(a, b), c) == Tally.merge(a, Tally.merge(b, c))
+    }
+}
+----
+
+The agent never reads the body of `add` or `merge`. The compile-time
+`CombinerChecker` is the guarantee that the annotation is trustworthy
+enough to treat as a specification.
+
+The same idea applies more broadly:
+
+[cols="2,3",options="header"]
+|===
+| Annotation                          | Derivable property
+
+| `@Associative` on `f(a, b)`         | `f(f(a, b), c) == f(a, f(b, c))`
+| `@Reducer(zero = z)` on `f(a, b)`   | `f(a, z) == a` and `f(z, a) == a`
+| `@Pure` (no `allows`)               | invocation idempotent, no observable 
effect
+| `@Modifies({ [fields...] })`        | every field not listed is bit-for-bit 
identical after the call
+|===
+
+This is the Groovy 6 design pitch in one sentence: declarations whose
+compile-time guarantee makes them safe for an agent to read as a
+spec, not as a comment.
+
+== How it stacks up
+
+For a JVM-resident FP audience, the comparison set is FunctionalJava,
+highj, and the language alternatives Scala and Kotlin.
+
+[cols="2,1,1,1,1,1",options="header"]
+|===
+| Concept | Groovy 6 | FunctionalJava | highj | Scala | Haskell
+
+| Closures and composition
+| native (`>>`, `<<`, curry)
+| `fj.F`, `.o()`, curry on `F2..F8`
+| `Functions` combinators
+| native (`andThen`, `compose`)
+| native
+
+| Monoid / Semigroup
+| `@Associative`/`@Reducer` + `CombinerChecker`
+| `Monoid<A>`, `Semigroup<A>` values
+| `Monoid<µ>` typeclass instance
+| `cats.Monoid` typeclass
+| `Monoid` class
+
+| Purity & effects
+| `@Pure(allows = ...)` + `PurityChecker`
+| n/a (convention only)
+| `IO<µ>` simulation
+| `cats.effect.IO`
+| `IO`, `State`, etc.
+
+| Frame conditions
+| `@Modifies` + `ModifiesChecker`
+| n/a
+| n/a
+| n/a (libraries)
+| n/a
+
+| Monadic comprehension
+| `DO` (GEP-23, no HKT)
+| n/a (hand-written `.bind`)
+| simulated, with µ tags
+| `for { ... } yield`
+| `do { ... }`
+
+| Null / absence
+| `NullChecker` (strict)
+| `fj.data.Option`
+| `Maybe<µ>`
+| `Option`
+| `Maybe`
+
+| Validation / errors
+| `fj.data.Validation` in `DO`
+| `Validation<E, A>`
+| `Either<µ, µ>`
+| `Either`, `Validated`
+| `Either`, `Validation`
+
+| Termination
+| `@Decreases` (Hoare)
+| n/a
+| n/a
+| n/a (libraries)
+| totality checker (extensions)
+
+| Higher-kinded abstraction
+| no
+| no (encoded by hand)
+| simulated HKT
+| native
+| native
+|===
+
+The point of the table is not that Groovy 6 ranks ahead of Scala or
+Haskell on any of these rows. It is that the column for the *checked*
+features used to be empty in Java's heritage — and now isn't.
+
+== Conclusion
+
+Groovy 6 is not asking you to think about endofunctors. It is asking
+you to declare what your methods do, accepting that the compiler will
+check, and giving you the notation to compose values across the carrier
+types you already use.
+
+* Monoid / Semigroup is now a *contract on a method*, not a wrapper
+type — verified by `CombinerChecker`.
+* `@Pure`, `@Modifies` and the contract annotations turn each method
+into a self-contained spec — a different shape from `IO`/`State`, the
+same reasoning payoff.
+* `DO` gives you the `for`/`do` notation across `Optional`, `Stream`,
+`CompletableFuture`, `Awaitable`, the FunctionalJava carriers and any
+user type with the right shape, without committing the language to
+higher-kinded types.
+* `NullChecker`, nested `copyWith`, destructuring, `val`,
+`@Decreases` and `@Invariant` fill the everyday gaps that send people
+to libraries.
+
+For new code on the JVM, the takeaway is that `@Pure`, `@Modifies`,
+`@Associative` and `@Reducer` cover most of what teams used
+FunctionalJava and highj for — and they do it on your own types,
+without inheritance and without lift/unlift. FunctionalJava and highj
+remain useful when you want the libraries' richer value-level
+combinators (notably `Validation` for applicative-style error
+accumulation, which Groovy 6 happily embeds in `DO`).
+
+== References
+
+* link:../wiki/GEP-23.html[GEP-23 — Monadic comprehensions]
+* link:../releasenotes/groovy-6.0.html#type-checking-extensions[Groovy 6 
release notes — type checking extensions]
+* link:../releasenotes/groovy-6.0.html#human-ai-reasoning[Groovy 6 release 
notes — Designed for Human and AI Reasoning]
+* link:../wiki/GEP-16.html[GEP-16 — `val` keyword]
+* link:../wiki/GEP-20.html[GEP-20 — Destructuring]
+* https://groovy.apache.org/blog/groovy-async-await[Native Async/Await for 
Groovy] — the `Awaitable` carrier `DO` composes
+* https://groovy.apache.org/blog/groovy-null-checker[NullChecker walkthrough]
+* https://www.functionaljava.org/[FunctionalJava]
+* https://github.com/highj/highj[highj — lightweight HKT for Java]
+* https://github.com/paulk-asert/groovy6-functional[Companion code for this 
post]
+
+.Update history
+****
+*20/May/2026*: Initial version.
+****

Reply via email to