@zio.dev/zio-blocks 0.0.33 → 0.0.51

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/guides/compile-time-resource-safety-with-scope.md +16 -17
  2. package/guides/getting-started-with-mux.md +1507 -0
  3. package/guides/query-dsl-extending.md +161 -102
  4. package/guides/query-dsl-fluent-builder.md +217 -157
  5. package/guides/query-dsl-reified-optics.md +12 -10
  6. package/guides/query-dsl-sql.md +246 -165
  7. package/guides/telemetry-guide.md +1069 -0
  8. package/guides/zio-schema-migration.md +29 -22
  9. package/index.md +292 -50
  10. package/package.json +1 -1
  11. package/plans/config-follow-up-prs.md +188 -0
  12. package/plans/config-pr-assessment-roadmap.md +310 -0
  13. package/reference/MuxDataFlow.jsx +250 -0
  14. package/reference/async.md +651 -0
  15. package/reference/chunk.md +3533 -308
  16. package/reference/codegen/case-class.md +436 -0
  17. package/reference/codegen/emitter-config.md +383 -0
  18. package/reference/codegen/examples.md +664 -0
  19. package/reference/codegen/field.md +316 -0
  20. package/reference/codegen/index.md +317 -0
  21. package/reference/codegen/scala-emitter.md +392 -0
  22. package/reference/codegen/scala-file.md +276 -0
  23. package/reference/codegen/sealed-trait.md +408 -0
  24. package/reference/codegen/type-definition.md +340 -0
  25. package/reference/codegen/type-ref.md +201 -0
  26. package/reference/combinators.md +347 -117
  27. package/reference/config.md +158 -0
  28. package/reference/context.md +4 -4
  29. package/reference/datastar.md +346 -0
  30. package/reference/docs.md +1461 -345
  31. package/reference/endpoint/auth-type.md +146 -0
  32. package/reference/endpoint/endpoint.md +297 -0
  33. package/reference/endpoint/http-codec.md +249 -0
  34. package/reference/endpoint/index.md +825 -0
  35. package/reference/endpoint/path-codec.md +237 -0
  36. package/reference/endpoint/route-pattern.md +196 -0
  37. package/reference/endpoint/route-tree.md +111 -0
  38. package/reference/endpoint/segment-codec.md +212 -0
  39. package/reference/html.md +1120 -0
  40. package/reference/htmx/attribute-values.md +359 -0
  41. package/reference/htmx/hx-encoding.md +111 -0
  42. package/reference/htmx/hx-params.md +204 -0
  43. package/reference/htmx/hx-swap.md +276 -0
  44. package/reference/htmx/hx-sync.md +251 -0
  45. package/reference/htmx/hx-target.md +314 -0
  46. package/reference/htmx/hx-trigger.md +457 -0
  47. package/reference/htmx/hx-url-update.md +239 -0
  48. package/reference/htmx/index.md +855 -0
  49. package/reference/http-model/index.md +47 -0
  50. package/reference/http-model/model.md +1481 -0
  51. package/reference/http-model/schema.md +747 -0
  52. package/reference/maybe.md +826 -0
  53. package/reference/media-type.md +2 -2
  54. package/reference/mux.mdx +823 -0
  55. package/reference/openapi.md +1351 -0
  56. package/reference/resource-management/defer-handle.md +1 -1
  57. package/reference/resource-management/resource.md +31 -2
  58. package/reference/resource-management/scope.md +28 -12
  59. package/reference/resource-management/wire.md +3 -7
  60. package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
  61. package/reference/ringbuffer/MpscDiagram.jsx +618 -0
  62. package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
  63. package/reference/ringbuffer/SpscDiagram.jsx +677 -0
  64. package/reference/ringbuffer/advanced.mdx +109 -0
  65. package/reference/ringbuffer/index.mdx +145 -0
  66. package/reference/ringbuffer/mpmc.mdx +151 -0
  67. package/reference/ringbuffer/mpsc.mdx +132 -0
  68. package/reference/ringbuffer/spmc.mdx +108 -0
  69. package/reference/ringbuffer/spsc.mdx +344 -0
  70. package/reference/{allows.md → schema/allows.md} +4 -4
  71. package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
  72. package/reference/{binding.md → schema/binding.md} +2 -3
  73. package/reference/schema/built-in-codecs/avro.md +451 -0
  74. package/reference/schema/built-in-codecs/bson.md +480 -0
  75. package/reference/schema/built-in-codecs/csv.md +564 -0
  76. package/reference/schema/built-in-codecs/index.md +77 -0
  77. package/reference/schema/built-in-codecs/json/index.md +295 -0
  78. package/reference/schema/built-in-codecs/json/json-config.md +217 -0
  79. package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
  80. package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
  81. package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
  82. package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
  83. package/reference/schema/built-in-codecs/messagepack.md +508 -0
  84. package/reference/schema/built-in-codecs/thrift.md +433 -0
  85. package/reference/schema/built-in-codecs/toon.md +1078 -0
  86. package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
  87. package/reference/schema/built-in-codecs/yaml.md +552 -0
  88. package/reference/{codec.md → schema/codec.md} +10 -10
  89. package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +151 -5
  90. package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
  91. package/reference/schema/format.md +92 -0
  92. package/reference/schema/index.md +50 -0
  93. package/reference/schema/migration.md +297 -0
  94. package/reference/{modifier.md → schema/modifier.md} +58 -7
  95. package/reference/{optics.md → schema/optics.md} +2 -2
  96. package/reference/{patch.md → schema/patch.md} +1 -1
  97. package/{path-interpolator.md → reference/schema/path-interpolator.md} +165 -72
  98. package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
  99. package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
  100. package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
  101. package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
  102. package/reference/{schema.md → schema/schema.md} +12 -0
  103. package/reference/{structural-types.md → schema/structural-types.md} +1 -1
  104. package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
  105. package/reference/smithy.md +533 -0
  106. package/reference/sql/db-codec-deriver.md +71 -0
  107. package/reference/sql/db-codec.md +687 -0
  108. package/reference/sql/db-con.md +271 -0
  109. package/reference/sql/db-connection.md +153 -0
  110. package/reference/sql/db-param-writer.md +77 -0
  111. package/reference/sql/db-param.md +66 -0
  112. package/reference/sql/db-result-reader.md +146 -0
  113. package/reference/sql/db-tx.md +82 -0
  114. package/reference/sql/db-value.md +41 -0
  115. package/reference/sql/ddl.md +85 -0
  116. package/reference/sql/frag.md +254 -0
  117. package/reference/sql/index.md +341 -0
  118. package/reference/sql/repo.md +600 -0
  119. package/reference/sql/sql-dialect.md +73 -0
  120. package/reference/sql/sql-logger.md +62 -0
  121. package/reference/sql/sql-name-mapper.md +70 -0
  122. package/reference/sql/table-metadata.md +134 -0
  123. package/reference/sql/table.md +448 -0
  124. package/reference/sql/transactor-zio.md +399 -0
  125. package/reference/sql/transactor.md +353 -0
  126. package/reference/sql-zio.md +112 -0
  127. package/reference/streams/concurrent-operators.md +106 -0
  128. package/reference/streams/index.md +653 -0
  129. package/reference/streams/pipeline.md +718 -0
  130. package/reference/streams/reader.md +1284 -0
  131. package/reference/streams/scala-2-compatibility.md +55 -0
  132. package/reference/streams/sink.md +1426 -0
  133. package/reference/streams/stream.md +2526 -0
  134. package/reference/streams/writer.md +1045 -0
  135. package/reference/streams/zero-boxing.md +275 -0
  136. package/reference/telemetry.md +693 -0
  137. package/reference/typeid.md +5 -19
  138. package/sidebars.js +238 -43
  139. package/reference/formats.md +0 -694
  140. package/reference/http-model.md +0 -1716
  141. package/reference/streams.md +0 -989
  142. package/ringbuffer.md +0 -249
  143. /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
  144. /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
  145. /package/reference/{lazy.md → schema/lazy.md} +0 -0
  146. /package/reference/{reflect.md → schema/reflect.md} +0 -0
  147. /package/reference/{registers.md → schema/registers.md} +0 -0
  148. /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
  149. /package/reference/{syntax.md → schema/syntax.md} +0 -0
  150. /package/reference/{validation.md → schema/validation.md} +0 -0
@@ -3,19 +3,24 @@ id: combinators
3
3
  title: "Combinators"
4
4
  ---
5
5
 
6
- The `combinators` module provides compile-time typeclasses for composing and decomposing values in type-safe ways. Each module focuses on a specific domain: tuples, Either types, and union types.
6
+ import Tabs from '@theme/Tabs';
7
+ import TabItem from '@theme/TabItem';
8
+
9
+ The `combinators` module provides compile-time typeclasses for composing and decomposing values in type-safe ways. Each module focuses on a specific domain: tuples, choices, concatenation widening, Either types, and union types.
7
10
 
8
11
  ## Overview
9
12
 
10
- The combinators module consists of three core modules:
13
+ The combinators module consists of five core modules:
11
14
 
12
15
  - **Tuples** - Tuple composition with automatic flattening and separation
16
+ - **Choices** - Cross-version branch construction and elimination over `|`
17
+ - **Concat** - Scala-2-only union-aware widening for sequential composition
13
18
  - **Eithers** - Either canonicalization to left-nested form
14
19
  - **Unions** - Union type operations (Scala 3 only)
15
20
 
16
21
  Each module provides:
17
- - A unified typeclass (e.g., `Tuples.Tuples[L, R]`) that provides both `combine` and `separate` operations
18
- - A convenience method `combine` (the `separate` operation is available on the typeclass instance)
22
+ - A unified typeclass (e.g., `Tuples.Tuples[L, R]`) that provides both `Tuples.Tuples#combine` and `Tuples.Tuples#separate` operations
23
+ - A convenience function like `Tuples.combine` (the `Tuples#separate` operation is available on the typeclass instance)
19
24
 
20
25
  All typeclasses are derived automatically via compile-time resolution and provide zero-cost abstractions.
21
26
 
@@ -23,39 +28,148 @@ All typeclasses are derived automatically via compile-time resolution and provid
23
28
 
24
29
  Add the following to your `build.sbt`:
25
30
 
26
- ```scala
27
- libraryDependencies += "dev.zio" %% "zio-blocks-combinators" % "<version>"
31
+ ```sbt
32
+ libraryDependencies += "dev.zio" %% "zio-blocks-combinators" % "0.0.51"
28
33
  ```
29
34
 
30
35
  For cross-platform projects (Scala.js):
31
36
 
32
- ```scala
33
- libraryDependencies += "dev.zio" %%% "zio-blocks-combinators" % "<version>"
37
+ ```sbt
38
+ libraryDependencies += "dev.zio" %%% "zio-blocks-combinators" % "0.0.51"
34
39
  ```
35
40
 
36
41
  Supported platforms:
37
- - **Tuples, Eithers**: JVM, Scala.js (Scala 2.13 and 3.x)
42
+ - **Tuples, Choices, Eithers**: JVM, Scala.js (Scala 2.13 and 3.x)
43
+ - **Concat**: Scala 2.13 only
38
44
  - **Unions**: JVM, Scala.js (Scala 3 only)
39
45
 
46
+ ## Motivation
47
+
48
+ Building type-safe, composable systems requires managing values and types at both runtime and compile time. The combinators module solves three distinct problems that arise in complex Scala applications:
49
+
50
+ ### The Tuple Nesting Problem
51
+
52
+ When building up results step-by-step—aggregating function parameters, accumulating intermediate results, or constructing compound values—you often end up with deeply nested tuples:
53
+
54
+ ```scala
55
+ // Manual nesting is tedious and error-prone
56
+ val step1 = (1, "a")
57
+ // step1: Tuple2[Int, String] = (1, "a")
58
+ val step2 = (step1, true)
59
+ // step2: Tuple2[Tuple2[Int, String], Boolean] = ((1, "a"), true)
60
+ val step3 = (step2, 3.14)
61
+ // step3: Tuple2[Tuple2[Tuple2[Int, String], Boolean], Double] = (
62
+ // ((1, "a"), true),
63
+ // 3.14
64
+ // )
65
+ val step4 = (step3, 'x')
66
+ // step4: Tuple2[Tuple2[Tuple2[Tuple2[Int, String], Boolean], Double], Char] = (
67
+ // (((1, "a"), true), 3.14),
68
+ // 'x'
69
+ // )
70
+ ```
71
+
72
+ This creates two problems:
73
+ 1. **Ergonomic burden**: Consumers of compound values must destructure deeply nested structures
74
+ 2. **Inconsistency**: Different code paths produce different tuple shapes, making composition fragile
75
+
76
+ The `Tuples` combinator automatically flattens these structures, producing clean, predictable tuples at each step.
77
+
78
+ ### The Either Canonicalization Problem
79
+
80
+ Error handling often involves composing multiple error types through Either chains. Without systematic canonicalization, Either types nest unpredictably:
81
+
82
+ ```scala
83
+ // Inconsistent nesting across code paths
84
+ val result1: Either[E1, V] = Left(e1)
85
+ val result2: Either[E1, Either[E2, V]] = Right(Left(e2))
86
+ val result3: Either[Either[E1, E2], V] = Right(Right(v))
87
+ // Each path has a different structure!
88
+ ```
89
+
90
+ This causes problems when:
91
+ 1. **Serializing error types** for schemas (each variant has a different shape)
92
+ 2. **Accumulating errors** (inconsistent nesting makes aggregation complex)
93
+ 3. **Pattern matching** (must handle multiple nesting patterns)
94
+
95
+ The `Eithers` combinator canonicalizes all Either types to a uniform left-nested form, ensuring systematic error composition.
96
+
97
+ ### The Scala 3 Union Type Gap
98
+
99
+ Scala 3 introduces native union types (`A | B`) that are more idiomatic than `Either[A, B]`. However, existing code, libraries, and serialization infrastructure are built around `Either`. When adopting Scala 3, you face a choice:
100
+
101
+ 1. Stick with `Either` for compatibility (missing idiomatic Scala 3 syntax)
102
+ 2. Switch to union types (breaking compatibility with Either-based code)
103
+ 3. Maintain two parallel type systems (duplication and cognitive overhead)
104
+
105
+ The `Unions` combinator bridges this gap, enabling bidirectional conversion between `Either[L, R]` and `L | R` with zero runtime overhead. Use union types idiomatically in your APIs while maintaining Either compatibility at serialization boundaries.
106
+
107
+ ## Quick Example
108
+
109
+ Here is how to combine multiple values and canonicalize error types:
110
+
111
+ ```scala
112
+ import zio.blocks.combinators.{Tuples, Eithers}
113
+
114
+ // Aggregate three values into a flattened tuple
115
+ val username: String = "alice"
116
+ // username: String = "alice"
117
+ val userId: Int = 42
118
+ // userId: Int = 42
119
+ val email: String = "alice@example.com"
120
+ // email: String = "alice@example.com"
121
+ val userTuple: Tuple3[String, Int, String] = Tuples.combine(username, Tuples.combine(userId, email))
122
+ // userTuple: Tuple3[String, Int, String] = ("alice", 42, "alice@example.com")
123
+
124
+ // Canonicalize nested Either types to left-nested form
125
+ val validationError: Either[String, Either[String, Boolean]] = Right(Left("invalid email"))
126
+ // validationError: Either[String, Either[String, Boolean]] = Right(
127
+ // Left("invalid email")
128
+ // )
129
+ val canonical : Either[Either[String, String], Boolean] = Eithers.combine(validationError)
130
+ // canonical: Either[Either[String, String], Boolean] = Left(
131
+ // Right("invalid email")
132
+ // )
133
+ ```
134
+
135
+ ## Concat (Scala 2 Only)
136
+
137
+ `Concat` is the Scala-2-only witness used by APIs such as `Stream.++` / `Stream.concat` to preserve Scala 3-style union behavior without introducing a separate operator.
138
+
139
+ Its rules are:
140
+
141
+ - same type => keep that type
142
+ - subtype + supertype => keep the supertype
143
+ - siblings with a unique meaningful common supertype => keep that supertype (e.g. `Dog` and `Cat` under sealed `Animal` collapse to `Animal`)
144
+ - otherwise (no shared meaningful supertype) => widen to `Either[L, R]` (the Scala 2 encoding of `L | R`)
145
+
146
+ For example, Scala 2 infers witnesses equivalent to these shapes:
147
+
148
+ ```scala
149
+ Concat.Concat.WithOut[Int, Int, Int]
150
+ Concat.Concat.WithOut[Dog, Animal, Animal]
151
+ Concat.Concat.WithOut[Dog, Cat, Animal]
152
+ Concat.Concat.WithOut[String, Int, Either[String, Int]]
153
+ ```
154
+
155
+ A common supertype is "meaningful" when it is something other than the noise types Scala 2's LUB inference produces by default (`Any`, `AnyRef`, `AnyVal`, `Object`, `Product`, `Serializable`, `java.io.Serializable`, `Comparable`). If filtering those parents leaves exactly one candidate, it becomes the result type — and the witness is identity-like, so callers such as `Stream.concat` reuse values bare without wrapping. Zero or multiple meaningful parents fall through to `Either`.
156
+
157
+ Unlike `Choices`, `Concat` is not usually called directly at runtime. It exists mainly so shared Scala-2 APIs can infer the same public result types that Scala 3 expresses with native unions.
158
+
40
159
  ## Tuples
41
160
 
42
161
  The `Tuples` module combines values into flat tuples and separates them back.
43
162
 
44
163
  ### combine
45
164
 
46
- `Tuples.Tuples[L, R]` combines two values into a flattened tuple.
165
+ To combine two values into a flattened tuple:
47
166
 
48
167
  ```scala
49
168
  import zio.blocks.combinators.Tuples
50
169
 
51
- // Basic combination
52
- val result1: (Int, String) = Tuples.combine(1, "hello")
53
-
54
- // Tuple flattening
55
- val result2: (Int, String, Boolean) = Tuples.combine((1, "hello"), true)
56
-
57
- // Deep flattening (Scala 3)
58
- val result3: (Int, String, Boolean, Double) = Tuples.combine((1, "hello"), (true, 3.14))
170
+ val result1 = Tuples.combine(1, "hello") // (1, "hello")
171
+ val result2 = Tuples.combine((1, "hello"), true) // (1, "hello", true)
172
+ val result3 = Tuples.combine((1, "hello"), (true, 3.14)) // (1, "hello", true, 3.14)j
59
173
  ```
60
174
 
61
175
  #### Identity Handling
@@ -65,14 +179,9 @@ Unit and EmptyTuple values are automatically eliminated:
65
179
  ```scala
66
180
  import zio.blocks.combinators.Tuples
67
181
 
68
- // Unit on left - returns right value
69
- val result1: Int = Tuples.combine((), 42)
70
-
71
- // Unit on right - returns left value
72
- val result2: String = Tuples.combine("hello", ())
73
-
74
- // EmptyTuple identity (Scala 3)
75
- val result3: String = Tuples.combine(EmptyTuple, "world")
182
+ Tuples.combine((), 42) // 42
183
+ Tuples.combine("hello", ()) // "hello"
184
+ Tuples.combine(EmptyTuple, "world") // "world"
76
185
  ```
77
186
 
78
187
  #### Tuple Flattening
@@ -82,64 +191,47 @@ Nested tuples are automatically flattened:
82
191
  ```scala
83
192
  import zio.blocks.combinators.Tuples
84
193
 
85
- // Tuple + value flattens to larger tuple
86
- val result1: (Int, String, Boolean) = Tuples.combine((1, "a"), true)
87
-
88
- // Tuple + tuple concatenates (Scala 3 - recursive flattening)
89
- val result2: (Int, String, Boolean, Double) = Tuples.combine((1, "a"), (true, 3.14))
90
-
91
- // Scala 2 - flattens left tuple only
92
- val result3: (Int, String, (Boolean, Double)) = Tuples.combine((1, "a"), (true, 3.14))
194
+ Tuples.combine((1, "a"), true) // (1, "a", true)
195
+ Tuples.combine((1, "a"), (true, 3.14)) // (1, "a", true, 3.14)
93
196
  ```
94
197
 
95
198
  ### separate
96
199
 
97
- `separate` is accessed via the unified typeclass instance and splits a tuple into its init (all but last) and last element.
200
+ To split a tuple into its init (all but last) and last element, access `Tuples#separate` via the unified typeclass instance:
98
201
 
99
202
  ```scala
100
203
  import zio.blocks.combinators.Tuples
101
204
 
102
- // 2-tuple separation
103
- val t2 = summon[Tuples.Tuples[Int, String]] // Scala 3
104
- // or: implicitly[Tuples.Tuples[Int, String]] // Scala 2
105
- val (left1, right1): (Int, String) = t2.separate((1, "hello"))
106
- // left1 = 1, right1 = "hello"
205
+ val t2 = summon[Tuples.Tuples[Int, String]]
206
+ t2.separate((1, "hello")) // ((1), "hello")
107
207
 
108
- // 3-tuple separation
109
208
  val t3 = summon[Tuples.Tuples[(Int, String), Boolean]]
110
- val (left2, right2): ((Int, String), Boolean) = t3.separate((1, "hello", true))
111
- // left2 = (1, "hello"), right2 = true
209
+ t3.separate((1, "hello", true)) // ((1, "hello"), true)
112
210
 
113
- // 4-tuple separation
114
211
  val t4 = summon[Tuples.Tuples[(Int, String, Boolean), Double]]
115
- val (left3, right3): ((Int, String, Boolean), Double) = t4.separate((1, "hello", true, 3.14))
116
- // left3 = (1, "hello", true), right3 = 3.14
117
-
118
-
212
+ t4.separate((1, "hello", true, 3.14)) // ((1, "hello", true), 3.14)
119
213
  ```
120
- ### Type-Level Operations
121
214
 
122
- The output type is computed at compile time via the `Out` type member:
215
+ When building recursive data structures like path codecs, `separate` decomposes combined tuples to process each segment independently:
123
216
 
124
217
  ```scala
125
218
  import zio.blocks.combinators.Tuples
126
219
 
127
- // Access the combiner with explicit output type
128
- val combiner: Tuples.Tuples.WithOut[Int, String, (Int, String)] =
129
- summon[Tuples.Tuples[Int, String]]
220
+ // Simulating recursive path encoding: a codec combines left and right path segments
221
+ case class PathSegment(name: String, value: String)
130
222
 
131
- // Access with explicit types
132
- val instance: Tuples.Tuples.WithOut[Int, String, (Int, String)] =
133
- summon[Tuples.Tuples[Int, String]]
134
- ```
135
-
136
- ### Scala 2 vs Scala 3 Differences
223
+ def encodePathSegment(combined: (String, String)): PathSegment = {
224
+ val tuples = summon[Tuples.Tuples[String, String]]
225
+ val (left, right) = tuples.separate(combined)
226
+ PathSegment(left, right)
227
+ }
137
228
 
138
- | Feature | Scala 2.13 | Scala 3.x |
139
- |---------|------------|-----------|
140
- | Maximum tuple arity | 22 | Unlimited |
141
- | Tuple flattening | Left tuple only | Recursive both sides |
142
- | EmptyTuple identity | Not available | Supported |
229
+ // Decompose a 3-element path into segments for recursive encoding
230
+ val path: (String, String, String) = ("users", "123", "profile")
231
+ val tuples3 = summon[Tuples.Tuples[(String, String), String]]
232
+ val (prefix, suffix) = tuples3.separate(path)
233
+ // prefix = ("users", "123"), suffix = "profile"
234
+ ```
143
235
 
144
236
  ## Eithers
145
237
 
@@ -147,22 +239,26 @@ The `Eithers` module canonicalizes Either types to left-nested form and separate
147
239
 
148
240
  ### combine
149
241
 
150
- `Eithers.Eithers[L, R]` transforms an `Either[L, R]` into its left-nested canonical form.
242
+ To transform an `Either[L, R]` into its left-nested canonical form:
151
243
 
152
244
  ```scala
153
245
  import zio.blocks.combinators.Eithers
154
246
 
155
247
  // Atomic Either - unchanged
156
- val result1: Either[Int, String] = Eithers.combine(Left(42): Either[Int, String])
248
+ Eithers.combine(Left(42): Either[Int, String])
249
+ // res5: Either[Int, String] = Left(42)
157
250
 
158
251
  // Right-nested Either - reassociates to left-nested
159
- val input: Either[Int, Either[String, Boolean]] = Right(Right(true))
160
- val result2: Either[Either[Int, String], Boolean] = Eithers.combine(input)
161
- // Right(Right(true)) becomes Right(true)
252
+ val input2 = Right(Right(true)): Either[Int, Either[String, Boolean]]
253
+ // input2: Either[Int, Either[String, Boolean]] = Right(Right(true))
254
+ Eithers.combine(input2)
255
+ // res6: Either[Either[Int, String], Boolean] = Right(true)
162
256
 
163
257
  // Left(42) becomes Left(Left(42))
164
- val input2: Either[Int, Either[String, Boolean]] = Left(42)
165
- val result3: Either[Either[Int, String], Boolean] = Eithers.combine(input2)
258
+ val input3 = Left(42): Either[Int, Either[String, Boolean]]
259
+ // input3: Either[Int, Either[String, Boolean]] = Left(42)
260
+ Eithers.combine(input3)
261
+ // res7: Either[Either[Int, String], Boolean] = Left(Left(42))
166
262
  ```
167
263
 
168
264
  #### Canonical Form
@@ -182,22 +278,74 @@ This transformation preserves values while reassociating the structure:
182
278
 
183
279
  ### separate
184
280
 
185
- `separate` is accessed via the unified typeclass instance and peels the rightmost alternative from a canonical Either:
281
+ `Eithers#separate` is accessed via the unified typeclass instance and reverses the canonicalization performed by `combine`. Together, they form a round-trip: canonicalizing to left-nested form and then separating back to the original structure:
186
282
 
187
283
  ```scala
188
284
  import zio.blocks.combinators.Eithers
189
285
 
190
286
  val e = summon[Eithers.Eithers[Int, String]]
191
- val input: Either[Int, String] = Left(42)
192
- val result: Either[Int, String] = e.separate(e.combine(input))
287
+ // e: Eithers[Int, String] {
288
+ type Out >: Either[Int, String] <: Either[Int, String]
289
+ } = zio.blocks.combinators.Eithers$Eithers$AtomicInstance@65d7b306
290
+ val input = Left(42): Either[Int, String]
291
+ // input: Either[Int, String] = Left(42)
292
+ e.separate(e.combine(input))
293
+ // res8: Either[Int, String] = Left(42)
193
294
  ```
194
295
 
195
- ### Use Cases
296
+ Use `separate` to decompose a canonical Either back to its original structure when you need to handle different error types differently:
297
+
298
+ ```scala
299
+ import zio.blocks.combinators.Eithers
196
300
 
197
- Eithers canonicalization is useful for:
198
- - **Schema sum type encoding** - Uniform representation of sealed traits
199
- - **Error handling composition** - Combining error types systematically
200
- - **Cross-version compatibility** - Works identically on Scala 2 and 3
301
+ sealed trait ValidationError
302
+ case class FieldError(field: String) extends ValidationError
303
+ case class FormatError(message: String) extends ValidationError
304
+
305
+ // You have a right-nested Either from multiple validation steps
306
+ val input: Either[FieldError, Either[FormatError, String]] = Right(Left(FormatError("invalid date")))
307
+
308
+ val eithers = summon[Eithers.Eithers[FieldError, Either[FormatError, String]]]
309
+ ```
310
+
311
+ Canonicalize to left-nested form for uniform processing, then reverse it to extract the original error types:
312
+
313
+ ```scala
314
+ // Original form: Either[FieldError, Either[FormatError, String]]
315
+ input
316
+ // res9: Either[FieldError, Either[FormatError, String]] = Right(
317
+ // Left(FormatError("invalid date"))
318
+ // )
319
+
320
+ // Canonicalize to left-nested form for uniform processing
321
+ val canonicalized = eithers.combine(input)
322
+ // canonicalized: Either[Either[FieldError, FormatError], String] = Left(
323
+ // Right(FormatError("invalid date"))
324
+ // )
325
+
326
+ // Reverse canonicalization to extract the original error types
327
+ val original = eithers.separate(canonicalized)
328
+ // original: Either[FieldError, Either[FormatError, String]] = Right(
329
+ // Left(FormatError("invalid date"))
330
+ // )
331
+
332
+ // Back to original form
333
+ original
334
+ // res10: Either[FieldError, Either[FormatError, String]] = Right(
335
+ // Left(FormatError("invalid date"))
336
+ // )
337
+ ```
338
+
339
+ Handle each error type independently:
340
+
341
+ ```scala
342
+ // Handle each error type independently
343
+ original match {
344
+ case Left(fieldErr: FieldError) => println(s"Field validation failed: ${fieldErr.field}")
345
+ case Right(Left(formatErr: FormatError)) => println(s"Format error: ${formatErr.message}")
346
+ case Right(Right(value)) => println(s"Valid: $value")
347
+ }
348
+ ```
201
349
 
202
350
  ## Unions (Scala 3 Only)
203
351
 
@@ -210,28 +358,32 @@ The `Unions` module converts between Either types and Scala 3 union types.
210
358
  ```scala
211
359
  import zio.blocks.combinators.Unions
212
360
 
213
- val either: Either[Int, String] = Left(42)
214
- val union: Int | String = Unions.combine(either)
215
- // Result: 42 (typed as Int | String)
361
+ val either1 = Left(42): Either[Int, String]
362
+ // either1: Either[Int, String] = Left(42)
363
+ Unions.combine(either1)
364
+ // res12: Int | String = 42
216
365
 
217
- val either2: Either[Int, String] = Right("hello")
218
- val union2: Int | String = Unions.combine(either2)
219
- // Result: "hello" (typed as Int | String)
366
+ val either2 = Right("hello"): Either[Int, String]
367
+ // either2: Either[Int, String] = Right("hello")
368
+ Unions.combine(either2)
369
+ // res13: Int | String = "hello"
220
370
  ```
221
371
 
222
372
  ### separate
223
373
 
224
- `separate` is accessed via the unified typeclass instance and discriminates a union type back to Either:
374
+ `Unions#separate` is accessed via the unified typeclass instance and discriminates a union type back to Either:
225
375
 
226
376
  ```scala
227
377
  import zio.blocks.combinators.Unions
228
378
 
229
379
  val u = summon[Unions.Unions.WithOut[Int, String, Int | String]]
230
- val result: Either[Int, String] = u.separate(42: Int | String)
231
- // Result: Left(42)
232
-
233
- val result2: Either[Int, String] = u.separate("hello": Int | String)
234
- // Result: Right("hello")
380
+ // u: Unions[Int, String] {
381
+ type Out >: Int | String <: Int | String
382
+ } = zio.blocks.combinators.Unions$Unions$UnionInstance@64a090cd
383
+ u.separate(42: Int | String)
384
+ // res14: Either[Int, String] = Left(42)
385
+ u.separate("hello": Int | String)
386
+ // res15: Either[Int, String] = Right("hello")
235
387
  ```
236
388
  ### Same-Type Rejection
237
389
 
@@ -253,8 +405,10 @@ val either: Either[Int, Int] = Left(1) // Distinguishable via Left/Right
253
405
  Union discrimination relies on runtime type tests, which are fragile for erased types:
254
406
 
255
407
  ```scala
408
+ import scala.collection.immutable.List
409
+
256
410
  // Problematic: List[Int] and List[String] erase to List
257
- val value: List[Int] | List[String] = List(1, 2, 3)
411
+ val problematicValue: List[Int] | List[String] = List(1, 2, 3)
258
412
  // Runtime cannot distinguish List[Int] from List[String]
259
413
 
260
414
  // Safe: Use distinct concrete types
@@ -263,7 +417,12 @@ val value: Int | String = 42 // Works reliably
263
417
 
264
418
  ## Generic Usage Patterns
265
419
 
266
- ### With Implicit Parameters (Scala 2)
420
+ The combinators module supports both Scala 2's implicit parameters and Scala 3's context parameters. Here are idiomatic usage patterns for each:
421
+
422
+ <Tabs groupId="scala-version" defaultValue="scala2">
423
+ <TabItem value="scala2" label="Scala 2">
424
+
425
+ To combine multiple values using implicit typeclass resolution:
267
426
 
268
427
  ```scala
269
428
  import zio.blocks.combinators.Tuples
@@ -280,7 +439,10 @@ val result = combineAll(1, "hello", true)
280
439
  // result: (Int, String, Boolean)
281
440
  ```
282
441
 
283
- ### With Context Parameters (Scala 3)
442
+ </TabItem>
443
+ <TabItem value="scala3" label="Scala 3">
444
+
445
+ To combine multiple values using context parameters:
284
446
 
285
447
  ```scala
286
448
  import zio.blocks.combinators.Tuples
@@ -296,6 +458,9 @@ val result = combineAll(1, "hello", true)
296
458
  // result: (Int, String, Boolean)
297
459
  ```
298
460
 
461
+ </TabItem>
462
+ </Tabs>
463
+
299
464
  ### Path-Dependent Types
300
465
 
301
466
  The `Out`, `Left`, and `Right` type members are path-dependent:
@@ -309,37 +474,102 @@ def process[L, R](l: L, r: R)(using t: Tuples.Tuples[L, R]): (L, R) =
309
474
  val result: (Int, String) = process(1, "hello")
310
475
  ```
311
476
 
312
- ### Type Aliases for Clarity
477
+ ## Integration Points
478
+
479
+ The combinator types integrate with other ZIO Blocks modules through systematic composition:
480
+
481
+ **Schema Evolution**: The `Eithers` canonicalization strategy directly supports schema sum type encoding. When deriving schemas for sealed trait hierarchies, the combinator ensures all Either encodings use the same left-nested form, enabling consistent serialization across schema variants.
482
+
483
+ **Error Handling**: `Eithers` provides a foundation for systematic error composition. Libraries building polymorphic error types can leverage canonicalization to ensure uniform error nesting, preventing subtle bugs from inconsistent Either structure.
484
+
485
+ **Scala 3 APIs**: The `Unions` type enables idiomatic Scala 3 DSLs and API designs that use native union syntax. Gateway types that convert between union-based and Either-based representations (e.g., for serialization compatibility) can use `Unions` for zero-cost interop.
486
+
487
+ **Tuple-Based Builders**: The `Tuples` module supports builder patterns and accumulator-based APIs that need to combine heterogeneous values step-by-step. By flattening automatically, it eliminates the ergonomic burden of manual nesting, making fluent builder chains natural.
488
+
489
+ ## Scala 2 vs Scala 3: Compatibility and Differences
490
+
491
+ The combinators module works across Scala 2.13 and Scala 3.x with **full source compatibility**. Write your code once; it compiles on both versions. However, certain features are version-specific due to language capabilities:
492
+
493
+ ### Tuples: Version Differences
494
+
495
+ **Scala 2.13 Limitations:**
496
+ - Maximum arity of 22 (the standard library tuple limit)
497
+ - Tuple flattening only works when the left argument is a tuple (right argument cannot be recursively flattened)
498
+ - No `EmptyTuple` type (use `Unit` as identity instead)
499
+
500
+ **Scala 3.x Enhancements:**
501
+ - Unlimited arity (tuples are truly variable-length)
502
+ - Recursive flattening on both sides: `Tuples.combine((1, "a"), (true, 3.14))` flattens both tuples into a 4-tuple
503
+ - `EmptyTuple` as a first-class type with proper identity semantics
504
+
505
+ **Example: The difference in practice**
506
+
507
+ <Tabs groupId="scala-version" defaultValue="scala2">
508
+ <TabItem value="scala2" label="Scala 2.13">
509
+
510
+ In Scala 2.13, combining two tuples on the right side fails to compile:
313
511
 
314
512
  ```scala
315
513
  import zio.blocks.combinators.Tuples
316
514
 
317
- // Typeclass with known output type
318
- type IntStringTuples = Tuples.Tuples.WithOut[Int, String, (Int, String)]
515
+ // ERROR: right side not flattened
516
+ val result = Tuples.combine((1, 2), (3, 4)) // Type mismatch
517
+ ```
518
+
519
+ </TabItem>
520
+ <TabItem value="scala3" label="Scala 3.x">
319
521
 
320
- // Typeclass with known left/right types
321
- type TripleTuples = Tuples.Tuples.WithOut[(Int, String), Boolean, (Int, String, Boolean)]
522
+ In Scala 3.x, recursive flattening on both sides works seamlessly:
523
+
524
+ ```scala
525
+ import zio.blocks.combinators.Tuples
526
+
527
+ // OK: both sides flattened
528
+ val result = Tuples.combine((1, 2), (3, 4)) // (1, 2, 3, 4)
322
529
  ```
323
530
 
324
- ## Performance Characteristics
531
+ </TabItem>
532
+ </Tabs>
533
+
534
+ ### Eithers: Full Cross-Version Support
535
+
536
+ `Eithers` canonicalization works identically on Scala 2.13 and 3.x. No version-specific behavior. Use with confidence across versions—canonicalization is deterministic.
537
+
538
+ ### Unions: Scala 3 Only
539
+
540
+ `Unions` requires Scala 3 because:
541
+ - Union types (`A | B`) are a Scala 3 language feature
542
+ - Runtime type tests (via `TypeTest`) are only available in Scala 3
543
+ - Scala 2 has no native union syntax
544
+
545
+ For Scala 2.13 codebases, use `Either` directly or `Eithers` canonicalization instead.
546
+
547
+ ### Feature Matrix
548
+
549
+ | Feature | Scala 2.13 | Scala 3.x | Notes |
550
+ |--------------------------------|------------|-----------|-------------------------------|
551
+ | **Tuples.combine** | ✅ | ✅ | Left-only flattening in 2.13 |
552
+ | **Tuples.separate** | ✅ | ✅ | Works identically on both |
553
+ | **Eithers.combine** | ✅ | ✅ | No differences |
554
+ | **Eithers.separate** | ✅ | ✅ | No differences |
555
+ | **Choices.left/right/separate**| ✅ | ✅ | Scala 2 uses `Either` alias |
556
+ | **Unions.combine** | ❌ | ✅ | Requires Scala 3 |
557
+ | **Unions.separate** | ❌ | ✅ | Requires Scala 3 |
558
+ | **EmptyTuple as identity** | ❌ | ✅ | Use `Unit` in Scala 2 |
559
+ | **Unlimited tuple arity** | ❌ | ✅ | Limited to 22 in Scala 2 |
560
+ | **Recursive tuple flattening** | ❌ | ✅ | Right side not flattened in 2 |
561
+
562
+ ### Migration Path from Scala 2 to 3
325
563
 
326
- | Module | Time Complexity | Notes |
327
- |--------|-----------------|-------|
328
- | Tuples.combine | O(1) to O(n) | O(1) for small tuples; O(n) for flattening nested tuples |
329
- | Tuples.separate | O(n) | Splits tuple at size-1 position |
330
- | Eithers.combine | O(d) | d = nesting depth of right-nested Either |
331
- | Eithers.separate | O(d) | Same as combine (delegates to combiner) |
332
- | Unions.combine | O(1) | Direct Either fold |
333
- | Unions.separate | O(1) | Single type test |
564
+ When adopting Scala 3, no changes are required for existing `Tuples` and `Eithers` code. Your code continues to work without modification. However, you can take advantage of new capabilities:
334
565
 
335
- All operations are pure and allocation-minimal.
566
+ 1. **Adopt `EmptyTuple` idiom**: Use `EmptyTuple` instead of `Unit` when combining with `Tuples` in Scala 3 for consistency with modern tuple syntax. Note that `Unit` remains fully supported and valid—`EmptyTuple` is a stylistic enhancement, not a replacement.
567
+ 2. **Simplify tuple builders**: Leverage recursive flattening on both sides to remove manual nesting. In Scala 3, `Tuples.combine((1, "a"), (true, 3.14))` automatically flattens to `(1, "a", true, 3.14)`.
568
+ 3. **Adopt `Choices` in shared code**: Use `Choices.left`, `Choices.right`, and `Choices.separate` when you want a single `|`-based API shape to compile on both Scala 2 and Scala 3.
569
+ 4. **Adopt `Unions` in Scala 3-only code**: Replace `Either` with union types in new Scala 3-only code for idiomatic syntax using the `Unions` combinator.
570
+ 5. **Gradual adoption**: Use `Choices` in cross-version modules and `Unions` in Scala-3-only modules. Convert between them at module boundaries using `Unions.combine` and `Unions.separate` as needed.
336
571
 
337
- ## Cross-Version Summary
572
+ ## See Also
338
573
 
339
- | Feature | Scala 2.13 | Scala 3.x |
340
- |---------|------------|-----------|
341
- | Tuples.Tuples | Yes (max 22) | Yes (unlimited) |
342
- | Eithers.Eithers | Yes | Yes |
343
- | Unions.Unions | No | Yes |
344
- | Recursive tuple flattening | No | Yes |
345
- | EmptyTuple handling | No | Yes |
574
+ - [Schema](./schema/schema.md) — The Schema module uses `Eithers` canonicalization for encoding sealed trait hierarchies and sum types with consistent Either nesting.
575
+ - **HTTP Model Schema** — When extracting multiple typed query parameters or headers in the HTTP Model schema module, `Eithers` provides systematic composition of error types for uniform error handling.