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 7406b74 add wire-car inspired example
7406b74 is described below
commit 7406b743c0a064e3ba958d511cf1f1ae30268ecb
Author: Paul King <[email protected]>
AuthorDate: Tue May 26 09:54:01 2026 +1000
add wire-car inspired example
---
site/src/site/blog/groovy6-functional.adoc | 170 ++++++++++++++++++++++++++++-
1 file changed, 169 insertions(+), 1 deletion(-)
diff --git a/site/src/site/blog/groovy6-functional.adoc
b/site/src/site/blog/groovy6-functional.adoc
index 70d1473..ffab524 100644
--- a/site/src/site/blog/groovy6-functional.adoc
+++ b/site/src/site/blog/groovy6-functional.adoc
@@ -631,6 +631,159 @@ and an `@AsResult` AST transform) recognised by `DO` for
the
runtime-composition path — all following the same producer-side /
consumer-side split.
+== Building your own checked DSL: a wire-diagram demo
+
+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 make that concrete, the companion repo contains a worked example.
+It ports the cartesian-categories idea from
+https://guaraqe.com/posts/2026-05-24-why-cartesian-categories.html[a recent
Haskell post]
+(`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.
+
+Producer side — a class of primitives, each declaring the env fields
+it reads and contributes:
+
+[source,groovy]
+----
+@WireSource
+class WordCountPrimitives {
+ @Wirable(outputs = ['path'])
+ static Map<String, ?> readPath() {
+ [path: System.getenv('WC_FILE') ?: defaultSample()]
+ }
+
+ @Wirable(inputs = ['path'], outputs = ['text'])
+ static Map<String, ?> loadText(String path) { [text: new File(path).text] }
+
+ @Wirable(inputs = ['text'], outputs = ['words'])
+ static Map<String, ?> countWords(String text) { [words:
text.split(/\s+/).findAll{ it }.size()] }
+
+ @Wirable(inputs = ['text'], outputs = ['lines'])
+ static Map<String, ?> countLines(String text) { [lines:
text.readLines().size()] }
+
+ @Wirable(inputs = ['text'], outputs = ['chars'])
+ static Map<String, ?> countChars(String text) { [chars: text.length()] }
+
+ @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"; [:]
+ }
+}
+----
+
+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:
+
+[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
+}
+
+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:
+
+[plantuml,WordCount,svg]
+----
+@startuml WordCount
+left to right direction
+skinparam shadowing false
+skinparam componentStyle rectangle
+skinparam component {
+ BackgroundColor #FAFAFA
+ 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
+loadText --> countWords : text
+loadText --> countLines : text
+loadText --> countChars : text
+readPath --> writeReport : path
+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.
+
== How it stacks up
For a JVM-resident FP audience, the comparison set is FunctionalJava,
@@ -745,6 +898,11 @@ 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.
For new code on the JVM, the takeaway is that `@Pure`, `@Modifies`,
`@Associative` and `@Reducer` cover most of what teams used
@@ -774,10 +932,20 @@ following the same two-layer pattern used elsewhere in
this post.
* https://www.functionaljava.org/[FunctionalJava]
* 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://github.com/paulk-asert/groovy6-functional[Companion code for this
post]
+* 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
.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.
+
*22/May/2026*: Vavr added to comparison set
and `DO` allow-list; producer-side / consumer-side framing for
declaration-driven features; `for await` vs `DO` distinction; GEP-24