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

Reply via email to