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

commit 32649455dac7737ce3591f29eb3d031a6683dc15
Author: Paul King <[email protected]>
AuthorDate: Tue May 26 19:22:14 2026 +1000

    add wire-car inspired example
---
 site/src/site/blog/groovy6-functional.adoc | 282 ++++++++++++++++++-----------
 1 file changed, 176 insertions(+), 106 deletions(-)

diff --git a/site/src/site/blog/groovy6-functional.adoc 
b/site/src/site/blog/groovy6-functional.adoc
index ffab524..03b8861 100644
--- a/site/src/site/blog/groovy6-functional.adoc
+++ b/site/src/site/blog/groovy6-functional.adoc
@@ -637,9 +637,9 @@ Every checker shown so far ships with Groovy. The same 
machinery —
 annotations, type-checking extensions, macros, runtime libraries —
 has always been open to anyone willing to write a `groovy.transform`
 analogue for their own domain. The producer/consumer split applies
-to user libraries too: you publish the annotation + the checker, your
-callers add `@TypeChecked(extensions = '…')` and write annotation-free
-code against your contract.
+to user libraries too: the library publishes the annotation, the
+checker and any macro; callers add `@TypeChecked(extensions = '…')`
+and write annotation-free code against the contract.
 
 To make that concrete, the companion repo contains a worked example.
 It ports the cartesian-categories idea from
@@ -647,62 +647,106 @@ 
https://guaraqe.com/posts/2026-05-24-why-cartesian-categories.html[a recent Hask
 (`WireCat`) into a small Groovy library that captures a composition
 of named primitives both as something *runnable* and as something
 *drawable*. The two views derive from one definition — the property
-WireCat preserves by forbidding `arr`, the property a tiny
-`@Wirable` annotation plus a `wire { }` builder preserves here.
+WireCat preserves by forbidding `arr`, the property a small
+`WIRE { }` macro preserves here.
 
-Producer side — a class of primitives, each declaring the env fields
-it reads and contributes:
+=== Producer side
+
+Plain Groovy methods. The macro reads the wiring from the call
+site, so the primitives carry no wire-specific annotation:
 
 [source,groovy]
 ----
-@WireSource
 class WordCountPrimitives {
-    @Wirable(outputs = ['path'])
-    static Map<String, ?> readPath() {
-        [path: System.getenv('WC_FILE') ?: defaultSample()]
+    static String readPath()              { System.getenv('WC_FILE') ?: 
defaultSample() }
+    static String loadText(String path)   { new File(path).text }
+    static int    countWords(String text) { text.split(/\s+/).findAll { it 
}.size() }
+    static int    countLines(String text) { text.readLines().size() }
+    static int    countChars(String text) { text.length() }
+    static void   writeReport(String path, int words, int lines, int chars) {
+        println "${path}: ${words} words, ${lines} lines, ${chars} chars"
     }
+}
+----
 
-    @Wirable(inputs = ['path'], outputs = ['text'])
-    static Map<String, ?> loadText(String path) { [text: new File(path).text] }
+=== Consumer side
 
-    @Wirable(inputs = ['text'], outputs = ['words'])
-    static Map<String, ?> countWords(String text) { [words: 
text.split(/\s+/).findAll{ it }.size()] }
+A proc-notation `WIRE { … }` macro using `<<` as the binding
+marker — the same operator Groovy 6 already uses for
+`DataflowVariable.bind`. Argument lists *are* the wiring, not
+visual sugar:
 
-    @Wirable(inputs = ['text'], outputs = ['lines'])
-    static Map<String, ?> countLines(String text) { [lines: 
text.readLines().size()] }
+[source,groovy]
+----
+import static groovy.wire.macro.WireMacroMethods.WIRE
+
+def wc = WIRE(WordCountPrimitives, 'WordCount', ['path']) {
+    text  << loadText(path)
+    words << countWords(text)
+    lines << countLines(text)
+    chars << countChars(text)
+    writeReport(path, words, lines, chars)
+}
+----
 
-    @Wirable(inputs = ['text'], outputs = ['chars'])
-    static Map<String, ?> countChars(String text) { [chars: text.length()] }
+At compile time the macro rewrites this body into literal Groovy 6
+dataflow code, auto-adding the `async { }` wrapping and the
+`df.` prefixes so each binding becomes a real
+`groovy.concurrent.DataflowVariable` write:
 
-    @Wirable(inputs = ['path', 'words', 'lines', 'chars'])
-    static Map<String, ?> writeReport(String path, int words, int lines, int 
chars) {
-        println "${path}: ${words} words, ${lines} lines, ${chars} chars"; [:]
-    }
+[source,groovy]
+----
+{ Dataflows __df ->
+    def __tasks = []
+    __tasks << async { __df.text  = WordCountPrimitives.loadText(__df.path) }
+    __tasks << async { __df.words = WordCountPrimitives.countWords(__df.text) }
+    __tasks << async { __df.lines = WordCountPrimitives.countLines(__df.text) }
+    __tasks << async { __df.chars = WordCountPrimitives.countChars(__df.text) }
+    __tasks << async { WordCountPrimitives.writeReport(__df.path, __df.words, 
__df.lines, __df.chars) }
+    __tasks.each { await(it) }
+    [path: __df.path, text: __df.text, words: __df.words, lines: __df.lines, 
chars: __df.chars]
 }
 ----
 
-Consumer side — a `wire { }` block composes the primitives. The
-wiring is implicit from the `@Wirable` declarations the primitives
-themselves carry; the builder fails fast if a step asks for a field
-no earlier step contributed:
+The macro is sugar over the idiomatic Groovy 6 form; nothing
+special happens at runtime. Concurrency, blocking reads, and
+single-assignment semantics are all the built-ins doing their
+normal job.
+
+The result is a `WireResult` with `.toPlantUml()` (the diagram,
+captured at compile time) and `.run(inputs)` (executes the
+auto-generated dataflow code, returns the bindings as a Map).
+
+The wire library has three pieces — an `@Wirable` annotation, a
+`WireChecker` type-checking extension, and the `WIRE` macro —
+usable individually or in combination. The macro form shown above
+uses just the macro. The companion repo also includes a direct
+`Wire.wire('name') { task X::y }` builder that takes method
+references to primitives carrying their wiring as annotations:
 
 [source,groovy]
 ----
-def wc = Wire.wire('WordCount') {
-    step WordCountPrimitives::readPath
-    step WordCountPrimitives::loadText
-    step WordCountPrimitives::countWords
-    step WordCountPrimitives::countLines
-    step WordCountPrimitives::countChars
-    step WordCountPrimitives::writeReport
-}
+@Wirable(inputs = ['path'], outputs = ['text'])
+static String loadText(String path) { new File(path).text }
+
+@Wirable(inputs = ['text'], outputs = ['words'])
+static int countWords(String text) { text.split(/\s+/).findAll { it }.size() }
 
-wc.run([:])                                 // executes the pipeline
-new File('WordCount.puml').text = wc.toPlantUml()   // structural form
+// …
 ----
 
-The structural form, generated *from the same `wc` value* — not
-hand-drawn, not maintained separately, regenerated on every build:
+Adding `@TypeChecked(extensions = 'groovy/wire/WireChecker.groovy')`
+to the method enclosing the `Wire.wire { … }` call lifts the same
+field-flow check from runtime to compile time. The three pieces
+layer freely — annotation alone for the runtime check, annotation
+plus checker for compile-time validation, or the macro when
+you'd rather skip the metadata altogether.
+
+=== The diagram, derived
+
+`wc.toPlantUml()` returns the structural view captured at the
+same compile-time pass that emitted the executor — not hand-drawn,
+not maintained separately, regenerated on every build:
 
 [plantuml,WordCount,svg]
 ----
@@ -715,74 +759,94 @@ skinparam component {
   BorderColor #777777
 }
 
-component "readPath" as readPath
 component "loadText" as loadText
 component "countWords" as countWords
 component "countLines" as countLines
 component "countChars" as countChars
 component "writeReport" as writeReport
 
-readPath --> loadText : path
+() "path" as in_path
+in_path --> loadText
 loadText --> countWords : text
 loadText --> countLines : text
 loadText --> countChars : text
-readPath --> writeReport : path
+in_path --> writeReport
 countWords --> writeReport : words
 countLines --> writeReport : lines
 countChars --> writeReport : chars
 @enduml
 ----
 
-The whole library is about 200 lines of Groovy across
-`Wirable`/`WireSource`/`WireDiagram` annotations, `WireNode` /
-`WireGraph` / `WireBuilder` runtime, and a `PlantUmlRenderer`. No
-compiler change, no GEP, no AST transform of its own — every piece is
-constructible from what Groovy 6 already gives you, the same way the
-built-in `@Reducer` / `CombinerChecker` family is constructed.
-
-Three things would lift the demo from "user library" to "feels native"
-without changing the library's external API:
-
-* *A compile-time `WireChecker`.* The fail-fast check in the builder
-is the runtime-deferred form of what a `@TypeChecked(extensions =
-'…WireChecker')` extension would do, shaped exactly like
-`CombinerChecker` / `PurityChecker` shown earlier. The same
-declarations on the same primitives — the diagnostic just moves
-from build time to compile time, and the IDE underlines it.
-
-* *A `WIRE { … }` macro form.* The builder reads cleanly, but a macro
-desugaring `WIRE { readPath(); loadText(path); … }` into the same
-`step` calls would lift the primitive invocations into the source
-language directly, closer to Haskell `do` / Scala `for` /
-WireCat's `proc`. Macros run at compile time, so the macro class
-must be on the classpath *before* the code that uses it is compiled
-— that is the demo's two-subproject layout (`wire/` defines the
-runtime, `wire-demo/` consumes it). The same constraint would
-apply to a macro.
-
-* *A `@RenderInGroovydoc` hook.* The `@WireDiagram(plantuml = '…')`
-annotation in the demo already holds the diagram in a
-machine-readable place; what is missing is a Groovydoc-side
-mechanism to embed annotation contents in the generated API doc.
-Pitched the same way as the rest of the post: declarations as
-specs, now also consumed by the docs tool. Until that hook exists,
-the demo writes the `.puml` to a sidecar file that asciidoc and
-markdown pipelines can include — exactly how this section above
-embeds the rendered diagram.
-
-None of those three need to land as language changes. The first is a
-type-checking extension (same SPI as the existing checkers). The
-second is a macro (same SPI as `DO`). The third is a Groovydoc
-enhancement, narrower in scope than a GEP. What the demo as it stands
-already shows is the more interesting claim: the *pattern* the post
-has been describing — declared contracts, compiler-enforced spec,
-producer/consumer split — transfers cleanly to library authors.
-
-The full code is in the
-https://github.com/paulk-asert/groovy6-functional/tree/main/wire[`wire/`]
-and
-https://github.com/paulk-asert/groovy6-functional/tree/main/wire-demo[`wire-demo/`]
-subprojects of the companion repo.
+=== What's checked, what isn't
+
+Three compile-time checks fire as the macro walks the body:
+
+* every reference must be bound — a graph-level input or the
+LHS of an earlier `<<`;
+* the using class must contain a method matching each call name;
+* at least one overload of that method must have the right arity.
+
+For example, `words << countWrods(text)` reports:
+
+[source]
+----
+WIRE: WordCountPrimitives has no method named 'countWrods'
+----
+
+`words << countWords(path, text)` reports:
+
+[source]
+----
+WIRE: WordCountPrimitives.countWords takes 1 arg(s),
+but called with 2
+----
+
+What remains a runtime concern: per-field type compatibility
+(does the `String` that producer `text` binds match the `String`
+parameter consumer `countWords` expects), and full type
+propagation through the DSL. Neither is hard — the first is
+roughly another 50 lines on the macro; the second is more
+substantial — and both are deferred for a follow-up.
+
+That partial-static-checking position is the same trade made
+elsewhere in this post: flow-sensitive analysis lives outside the
+type system (cf. `NullChecker(strict: true)`); declarations carry
+the contract; the type stays simple and the checker does the work.
+
+=== One thing still missing
+
+The companion repo writes the generated PlantUML to a sidecar
+`.puml` file that asciidoctor (`[plantuml]` blocks above) and
+markdown pipelines can include directly. The natural next step is
+a Groovydoc hook — say `@RenderInGroovydoc` as a meta-annotation
+on the annotation type — that would let an API doc embed the same
+PlantUML from a method's `@WireDiagram(plantuml = '…')` annotation
+without a separate file. Pitched the same way as everything else
+here: declarations as specs, now consumed by the docs tool too.
+Strictly smaller than a GEP, narrower than a language change.
+
+=== Where the code lives
+
+Six small subprojects under
+https://github.com/paulk-asert/groovy6-functional[`groovy6-functional`]:
+
+* https://github.com/paulk-asert/groovy6-functional/tree/main/wire[`wire/`]
+— annotations (`@Wirable`, `@WireSource`, `@WireDiagram`), builder DSL,
+dataflow runtime, PlantUML renderer.
+* 
https://github.com/paulk-asert/groovy6-functional/tree/main/wire-checker[`wire-checker/`]
+— the `WireChecker` type-checking extension.
+* 
https://github.com/paulk-asert/groovy6-functional/tree/main/wire-macro[`wire-macro/`]
+— the `WIRE` `@Macro` method plus its DGM extension descriptor.
+* 
https://github.com/paulk-asert/groovy6-functional/tree/main/wire-demo[`wire-demo/`],
+https://github.com/paulk-asert/groovy6-functional/tree/main/wire-demo-checked[`wire-demo-checked/`],
+https://github.com/paulk-asert/groovy6-functional/tree/main/wire-demo-macro[`wire-demo-macro/`]
+— consumer demos for the builder, builder-with-checker, and macro
+forms respectively.
+
+No compiler change, no GEP. The pattern this post has been describing
+— declared contracts, compiler-enforced spec, producer/consumer split
+— transfers cleanly to library authors using only the SPI hooks the
+Groovy team uses to build the in-tree checkers.
 
 == How it stacks up
 
@@ -898,11 +962,12 @@ higher-kinded types.
 * `NullChecker`, nested `copyWith`, destructuring, `val`,
 `@Decreases` and `@Invariant` fill the everyday gaps that send people
 to libraries.
-* The same annotation + checker + builder + macro pattern the language
-team used for those features is open to library authors: the
-`@Wirable` / `wire { }` / PlantUML example in the companion repo
-ports the WireCat cartesian-categories idea in around 200 lines, no
-GEP required.
+* The same extensibility hooks the language team used for the
+in-tree checkers are open to library authors: a `WIRE { name <<
+call(args) }` proc-notation macro that compiles to literal
+`async { df.X = … }` Groovy 6 dataflow code, with bound-name,
+existence and arity checks at expansion time, ports the
+WireCat cartesian-categories idea on the same SPI surface.
 
 For new code on the JVM, the takeaway is that `@Pure`, `@Modifies`,
 `@Associative` and `@Reducer` cover most of what teams used
@@ -933,18 +998,23 @@ following the same two-layer pattern used elsewhere in 
this post.
 * https://github.com/highj/highj[highj — lightweight HKT for Java]
 * https://vavr.io/[Vavr] — Java FP library with 
`Try`/`Either`/`Validation`/`Option` control carriers, persistent collections, 
tuples, and the `For(...).yield(...)` For-Comprehension form. The control 
carriers are in `DO`'s standard allow-list (Groovy 6).
 * https://guaraqe.com/posts/2026-05-24-why-cartesian-categories.html[WireCat: 
Visual Programming with Cartesian Categories] — the Haskell post that the 
`wire` demo is modelled on
-* https://github.com/paulk-asert/groovy6-functional[Companion code for this 
post] — see the
-https://github.com/paulk-asert/groovy6-functional/tree/main/wire[`wire/`] and
-https://github.com/paulk-asert/groovy6-functional/tree/main/wire-demo[`wire-demo/`]
-subprojects for the user-built checked-DSL example
+* https://github.com/paulk-asert/groovy6-functional[Companion code for this 
post] — six `wire-*`
+subprojects covering the macro form (shown above) plus the
+builder and builder-with-checker variants
 
 .Update history
 ****
-*26/May/2026*: Added "Building your own checked DSL" section with a
-`@Wirable` + `wire { }` + PlantUML worked example (companion
-`wire/` and `wire-demo/` subprojects), modelled on the cartesian
-categories from the recent WireCat post — making the producer/consumer
-split transferable to library authors.
+*26/May/2026*: Added "Building your own checked DSL" section
+demonstrating the producer/consumer split transferred to library
+authors. The headline form is a `WIRE` macro using `<<` (the
+`DataflowVariable.bind` operator) that auto-adds `async { }`
+wrapping and `df.` prefixes — sugar over literal Groovy 6
+dataflow code rather than a competing DSL. Bound-name,
+method-existence and arity checks fire at macro-expansion time
+with source-located errors. The companion repo additionally
+carries an `@Wirable`-driven builder and a `WireChecker`
+type-checking extension across six subprojects. Worked example
+ported from the recent WireCat Haskell post.
 
 *22/May/2026*: Vavr added to comparison set
 and `DO` allow-list; producer-side / consumer-side framing for

Reply via email to