@zio.dev/zio-blocks 0.0.28 → 0.0.29

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.
@@ -0,0 +1,345 @@
1
+ ---
2
+ id: combinators
3
+ title: "Combinators"
4
+ ---
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.
7
+
8
+ ## Overview
9
+
10
+ The combinators module consists of three core modules:
11
+
12
+ - **Tuples** - Tuple composition with automatic flattening and separation
13
+ - **Eithers** - Either canonicalization to left-nested form
14
+ - **Unions** - Union type operations (Scala 3 only)
15
+
16
+ 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)
19
+
20
+ All typeclasses are derived automatically via compile-time resolution and provide zero-cost abstractions.
21
+
22
+ ## Installation
23
+
24
+ Add the following to your `build.sbt`:
25
+
26
+ ```scala
27
+ libraryDependencies += "dev.zio" %% "zio-blocks-combinators" % "<version>"
28
+ ```
29
+
30
+ For cross-platform projects (Scala.js):
31
+
32
+ ```scala
33
+ libraryDependencies += "dev.zio" %%% "zio-blocks-combinators" % "<version>"
34
+ ```
35
+
36
+ Supported platforms:
37
+ - **Tuples, Eithers**: JVM, Scala.js (Scala 2.13 and 3.x)
38
+ - **Unions**: JVM, Scala.js (Scala 3 only)
39
+
40
+ ## Tuples
41
+
42
+ The `Tuples` module combines values into flat tuples and separates them back.
43
+
44
+ ### combine
45
+
46
+ `Tuples.Tuples[L, R]` combines two values into a flattened tuple.
47
+
48
+ ```scala
49
+ import zio.blocks.combinators.Tuples
50
+
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))
59
+ ```
60
+
61
+ #### Identity Handling
62
+
63
+ Unit and EmptyTuple values are automatically eliminated:
64
+
65
+ ```scala
66
+ import zio.blocks.combinators.Tuples
67
+
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")
76
+ ```
77
+
78
+ #### Tuple Flattening
79
+
80
+ Nested tuples are automatically flattened:
81
+
82
+ ```scala
83
+ import zio.blocks.combinators.Tuples
84
+
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))
93
+ ```
94
+
95
+ ### separate
96
+
97
+ `separate` is accessed via the unified typeclass instance and splits a tuple into its init (all but last) and last element.
98
+
99
+ ```scala
100
+ import zio.blocks.combinators.Tuples
101
+
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"
107
+
108
+ // 3-tuple separation
109
+ 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
112
+
113
+ // 4-tuple separation
114
+ 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
+
119
+ ```
120
+ ### Type-Level Operations
121
+
122
+ The output type is computed at compile time via the `Out` type member:
123
+
124
+ ```scala
125
+ import zio.blocks.combinators.Tuples
126
+
127
+ // Access the combiner with explicit output type
128
+ val combiner: Tuples.Tuples.WithOut[Int, String, (Int, String)] =
129
+ summon[Tuples.Tuples[Int, String]]
130
+
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
137
+
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 |
143
+
144
+ ## Eithers
145
+
146
+ The `Eithers` module canonicalizes Either types to left-nested form and separates them.
147
+
148
+ ### combine
149
+
150
+ `Eithers.Eithers[L, R]` transforms an `Either[L, R]` into its left-nested canonical form.
151
+
152
+ ```scala
153
+ import zio.blocks.combinators.Eithers
154
+
155
+ // Atomic Either - unchanged
156
+ val result1: Either[Int, String] = Eithers.combine(Left(42): Either[Int, String])
157
+
158
+ // 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)
162
+
163
+ // 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)
166
+ ```
167
+
168
+ #### Canonical Form
169
+
170
+ The canonical form is always left-nested:
171
+
172
+ ```
173
+ Right-nested input: Left-nested output:
174
+ Either[A, Either[B, C]] => Either[Either[A, B], C]
175
+ Either[A, Either[B, Either[C, D]]] => Either[Either[Either[A, B], C], D]
176
+ ```
177
+
178
+ This transformation preserves values while reassociating the structure:
179
+ - `Left(a)` → `Left(Left(a))`
180
+ - `Right(Left(b))` → `Left(Right(b))`
181
+ - `Right(Right(c))` → `Right(c)`
182
+
183
+ ### separate
184
+
185
+ `separate` is accessed via the unified typeclass instance and peels the rightmost alternative from a canonical Either:
186
+
187
+ ```scala
188
+ import zio.blocks.combinators.Eithers
189
+
190
+ 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))
193
+ ```
194
+
195
+ ### Use Cases
196
+
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
201
+
202
+ ## Unions (Scala 3 Only)
203
+
204
+ The `Unions` module converts between Either types and Scala 3 union types.
205
+
206
+ ### combine
207
+
208
+ `Unions.Unions[L, R]` converts an `Either[L, R]` to a union type `L | R`:
209
+
210
+ ```scala
211
+ import zio.blocks.combinators.Unions
212
+
213
+ val either: Either[Int, String] = Left(42)
214
+ val union: Int | String = Unions.combine(either)
215
+ // Result: 42 (typed as Int | String)
216
+
217
+ val either2: Either[Int, String] = Right("hello")
218
+ val union2: Int | String = Unions.combine(either2)
219
+ // Result: "hello" (typed as Int | String)
220
+ ```
221
+
222
+ ### separate
223
+
224
+ `separate` is accessed via the unified typeclass instance and discriminates a union type back to Either:
225
+
226
+ ```scala
227
+ import zio.blocks.combinators.Unions
228
+
229
+ 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")
235
+ ```
236
+ ### Same-Type Rejection
237
+
238
+ Union types collapse same types (`A | A` = `A`), making them ambiguous. The separator rejects overlapping types at compile time:
239
+
240
+ ```scala
241
+ import zio.blocks.combinators.Unions
242
+
243
+ // Compile error: Union types must contain unique types
244
+ // val u = summon[Unions.Unions.WithOut[Int, Int, Int | Int]]
245
+
246
+ // Use Either for same-type alternation instead:
247
+ import zio.blocks.combinators.Eithers
248
+ val either: Either[Int, Int] = Left(1) // Distinguishable via Left/Right
249
+ ```
250
+
251
+ ### Type Erasure Caveat
252
+
253
+ Union discrimination relies on runtime type tests, which are fragile for erased types:
254
+
255
+ ```scala
256
+ // Problematic: List[Int] and List[String] erase to List
257
+ val value: List[Int] | List[String] = List(1, 2, 3)
258
+ // Runtime cannot distinguish List[Int] from List[String]
259
+
260
+ // Safe: Use distinct concrete types
261
+ val value: Int | String = 42 // Works reliably
262
+ ```
263
+
264
+ ## Generic Usage Patterns
265
+
266
+ ### With Implicit Parameters (Scala 2)
267
+
268
+ ```scala
269
+ import zio.blocks.combinators.Tuples
270
+
271
+ def combineAll[A, B, C](a: A, b: B, c: C)(
272
+ implicit ab: Tuples.Tuples[A, B],
273
+ abc: Tuples.Tuples[ab.Out, C]
274
+ ): abc.Out = {
275
+ val step1 = ab.combine(a, b)
276
+ abc.combine(step1, c)
277
+ }
278
+
279
+ val result = combineAll(1, "hello", true)
280
+ // result: (Int, String, Boolean)
281
+ ```
282
+
283
+ ### With Context Parameters (Scala 3)
284
+
285
+ ```scala
286
+ import zio.blocks.combinators.Tuples
287
+
288
+ def combineAll[A, B, C](a: A, b: B, c: C)(using
289
+ ab: Tuples.Tuples[A, B],
290
+ abc: Tuples.Tuples[ab.Out, C]
291
+ ): abc.Out =
292
+ val step1 = ab.combine(a, b)
293
+ abc.combine(step1, c)
294
+
295
+ val result = combineAll(1, "hello", true)
296
+ // result: (Int, String, Boolean)
297
+ ```
298
+
299
+ ### Path-Dependent Types
300
+
301
+ The `Out`, `Left`, and `Right` type members are path-dependent:
302
+
303
+ ```scala
304
+ import zio.blocks.combinators.Tuples
305
+
306
+ def process[L, R](l: L, r: R)(using t: Tuples.Tuples[L, R]): (L, R) =
307
+ t.separate(t.combine(l, r))
308
+
309
+ val result: (Int, String) = process(1, "hello")
310
+ ```
311
+
312
+ ### Type Aliases for Clarity
313
+
314
+ ```scala
315
+ import zio.blocks.combinators.Tuples
316
+
317
+ // Typeclass with known output type
318
+ type IntStringTuples = Tuples.Tuples.WithOut[Int, String, (Int, String)]
319
+
320
+ // Typeclass with known left/right types
321
+ type TripleTuples = Tuples.Tuples.WithOut[(Int, String), Boolean, (Int, String, Boolean)]
322
+ ```
323
+
324
+ ## Performance Characteristics
325
+
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 |
334
+
335
+ All operations are pure and allocation-minimal.
336
+
337
+ ## Cross-Version Summary
338
+
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 |