@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.
- package/guides/compile-time-resource-safety-with-scope.md +16 -17
- package/guides/getting-started-with-mux.md +1507 -0
- package/guides/query-dsl-extending.md +161 -102
- package/guides/query-dsl-fluent-builder.md +217 -157
- package/guides/query-dsl-reified-optics.md +12 -10
- package/guides/query-dsl-sql.md +246 -165
- package/guides/telemetry-guide.md +1069 -0
- package/guides/zio-schema-migration.md +29 -22
- package/index.md +292 -50
- package/package.json +1 -1
- package/plans/config-follow-up-prs.md +188 -0
- package/plans/config-pr-assessment-roadmap.md +310 -0
- package/reference/MuxDataFlow.jsx +250 -0
- package/reference/async.md +651 -0
- package/reference/chunk.md +3533 -308
- package/reference/codegen/case-class.md +436 -0
- package/reference/codegen/emitter-config.md +383 -0
- package/reference/codegen/examples.md +664 -0
- package/reference/codegen/field.md +316 -0
- package/reference/codegen/index.md +317 -0
- package/reference/codegen/scala-emitter.md +392 -0
- package/reference/codegen/scala-file.md +276 -0
- package/reference/codegen/sealed-trait.md +408 -0
- package/reference/codegen/type-definition.md +340 -0
- package/reference/codegen/type-ref.md +201 -0
- package/reference/combinators.md +347 -117
- package/reference/config.md +158 -0
- package/reference/context.md +4 -4
- package/reference/datastar.md +346 -0
- package/reference/docs.md +1461 -345
- package/reference/endpoint/auth-type.md +146 -0
- package/reference/endpoint/endpoint.md +297 -0
- package/reference/endpoint/http-codec.md +249 -0
- package/reference/endpoint/index.md +825 -0
- package/reference/endpoint/path-codec.md +237 -0
- package/reference/endpoint/route-pattern.md +196 -0
- package/reference/endpoint/route-tree.md +111 -0
- package/reference/endpoint/segment-codec.md +212 -0
- package/reference/html.md +1120 -0
- package/reference/htmx/attribute-values.md +359 -0
- package/reference/htmx/hx-encoding.md +111 -0
- package/reference/htmx/hx-params.md +204 -0
- package/reference/htmx/hx-swap.md +276 -0
- package/reference/htmx/hx-sync.md +251 -0
- package/reference/htmx/hx-target.md +314 -0
- package/reference/htmx/hx-trigger.md +457 -0
- package/reference/htmx/hx-url-update.md +239 -0
- package/reference/htmx/index.md +855 -0
- package/reference/http-model/index.md +47 -0
- package/reference/http-model/model.md +1481 -0
- package/reference/http-model/schema.md +747 -0
- package/reference/maybe.md +826 -0
- package/reference/media-type.md +2 -2
- package/reference/mux.mdx +823 -0
- package/reference/openapi.md +1351 -0
- package/reference/resource-management/defer-handle.md +1 -1
- package/reference/resource-management/resource.md +31 -2
- package/reference/resource-management/scope.md +28 -12
- package/reference/resource-management/wire.md +3 -7
- package/reference/ringbuffer/MpmcDiagram.jsx +717 -0
- package/reference/ringbuffer/MpscDiagram.jsx +618 -0
- package/reference/ringbuffer/SpmcDiagram.jsx +680 -0
- package/reference/ringbuffer/SpscDiagram.jsx +677 -0
- package/reference/ringbuffer/advanced.mdx +109 -0
- package/reference/ringbuffer/index.mdx +145 -0
- package/reference/ringbuffer/mpmc.mdx +151 -0
- package/reference/ringbuffer/mpsc.mdx +132 -0
- package/reference/ringbuffer/spmc.mdx +108 -0
- package/reference/ringbuffer/spsc.mdx +344 -0
- package/reference/{allows.md → schema/allows.md} +4 -4
- package/reference/{binding-resolver.md → schema/binding-resolver.md} +1 -1
- package/reference/{binding.md → schema/binding.md} +2 -3
- package/reference/schema/built-in-codecs/avro.md +451 -0
- package/reference/schema/built-in-codecs/bson.md +480 -0
- package/reference/schema/built-in-codecs/csv.md +564 -0
- package/reference/schema/built-in-codecs/index.md +77 -0
- package/reference/schema/built-in-codecs/json/index.md +295 -0
- package/reference/schema/built-in-codecs/json/json-config.md +217 -0
- package/reference/{json-patch.md → schema/built-in-codecs/json/json-patch.md} +5 -5
- package/reference/{json-schema.md → schema/built-in-codecs/json/json-schema.md} +14 -47
- package/reference/schema/built-in-codecs/json/json-selection.md +322 -0
- package/reference/{json.md → schema/built-in-codecs/json/json.md} +32 -64
- package/reference/schema/built-in-codecs/messagepack.md +508 -0
- package/reference/schema/built-in-codecs/thrift.md +433 -0
- package/reference/schema/built-in-codecs/toon.md +1078 -0
- package/reference/{xml.md → schema/built-in-codecs/xml.md} +13 -9
- package/reference/schema/built-in-codecs/yaml.md +552 -0
- package/reference/{codec.md → schema/codec.md} +10 -10
- package/reference/{dynamic-optic.md → schema/dynamic-optic.md} +151 -5
- package/reference/{dynamic-schema.md → schema/dynamic-schema.md} +8 -8
- package/reference/schema/format.md +92 -0
- package/reference/schema/index.md +50 -0
- package/reference/schema/migration.md +297 -0
- package/reference/{modifier.md → schema/modifier.md} +58 -7
- package/reference/{optics.md → schema/optics.md} +2 -2
- package/reference/{patch.md → schema/patch.md} +1 -1
- package/{path-interpolator.md → reference/schema/path-interpolator.md} +165 -72
- package/reference/{schema-evolution → schema/schema-evolution}/as.md +8 -8
- package/reference/{schema-evolution → schema/schema-evolution}/index.md +2 -2
- package/reference/{schema-evolution → schema/schema-evolution}/into.md +8 -8
- package/reference/{schema-expr.md → schema/schema-expr.md} +110 -175
- package/reference/{schema.md → schema/schema.md} +12 -0
- package/reference/{structural-types.md → schema/structural-types.md} +1 -1
- package/reference/{type-class-derivation.md → schema/type-class-derivation.md} +63 -1
- package/reference/smithy.md +533 -0
- package/reference/sql/db-codec-deriver.md +71 -0
- package/reference/sql/db-codec.md +687 -0
- package/reference/sql/db-con.md +271 -0
- package/reference/sql/db-connection.md +153 -0
- package/reference/sql/db-param-writer.md +77 -0
- package/reference/sql/db-param.md +66 -0
- package/reference/sql/db-result-reader.md +146 -0
- package/reference/sql/db-tx.md +82 -0
- package/reference/sql/db-value.md +41 -0
- package/reference/sql/ddl.md +85 -0
- package/reference/sql/frag.md +254 -0
- package/reference/sql/index.md +341 -0
- package/reference/sql/repo.md +600 -0
- package/reference/sql/sql-dialect.md +73 -0
- package/reference/sql/sql-logger.md +62 -0
- package/reference/sql/sql-name-mapper.md +70 -0
- package/reference/sql/table-metadata.md +134 -0
- package/reference/sql/table.md +448 -0
- package/reference/sql/transactor-zio.md +399 -0
- package/reference/sql/transactor.md +353 -0
- package/reference/sql-zio.md +112 -0
- package/reference/streams/concurrent-operators.md +106 -0
- package/reference/streams/index.md +653 -0
- package/reference/streams/pipeline.md +718 -0
- package/reference/streams/reader.md +1284 -0
- package/reference/streams/scala-2-compatibility.md +55 -0
- package/reference/streams/sink.md +1426 -0
- package/reference/streams/stream.md +2526 -0
- package/reference/streams/writer.md +1045 -0
- package/reference/streams/zero-boxing.md +275 -0
- package/reference/telemetry.md +693 -0
- package/reference/typeid.md +5 -19
- package/sidebars.js +238 -43
- package/reference/formats.md +0 -694
- package/reference/http-model.md +0 -1716
- package/reference/streams.md +0 -989
- package/ringbuffer.md +0 -249
- /package/reference/{json-differ.md → schema/built-in-codecs/json/json-differ.md} +0 -0
- /package/reference/{dynamic-value.md → schema/dynamic-value.md} +0 -0
- /package/reference/{lazy.md → schema/lazy.md} +0 -0
- /package/reference/{reflect.md → schema/reflect.md} +0 -0
- /package/reference/{registers.md → schema/registers.md} +0 -0
- /package/reference/{schema-error.md → schema/schema-error.md} +0 -0
- /package/reference/{syntax.md → schema/syntax.md} +0 -0
- /package/reference/{validation.md → schema/validation.md} +0 -0
package/reference/combinators.md
CHANGED
|
@@ -3,19 +3,24 @@ id: combinators
|
|
|
3
3
|
title: "Combinators"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
|
|
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
|
|
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
|
|
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
|
-
```
|
|
27
|
-
libraryDependencies += "dev.zio" %% "zio-blocks-combinators" % "
|
|
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
|
-
```
|
|
33
|
-
libraryDependencies += "dev.zio" %%% "zio-blocks-combinators" % "
|
|
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
|
-
|
|
165
|
+
To combine two values into a flattened tuple:
|
|
47
166
|
|
|
48
167
|
```scala
|
|
49
168
|
import zio.blocks.combinators.Tuples
|
|
50
169
|
|
|
51
|
-
//
|
|
52
|
-
val
|
|
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
|
-
//
|
|
69
|
-
|
|
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
|
-
|
|
86
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
128
|
-
|
|
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
|
-
|
|
132
|
-
val
|
|
133
|
-
|
|
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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
165
|
-
|
|
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
|
|
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
|
-
|
|
192
|
-
|
|
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
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
214
|
-
|
|
215
|
-
|
|
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]
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
318
|
-
|
|
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
|
-
|
|
321
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
572
|
+
## See Also
|
|
338
573
|
|
|
339
|
-
|
|
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.
|