@zio.dev/zio-blocks 0.0.1

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,364 @@
1
+ ---
2
+ id: binding
3
+ title: "The Binding Data Type"
4
+ sidebar_label: Binding
5
+ ---
6
+
7
+ `Binding` is a sealed trait in ZIO Blocks that provides the operational machinery for constructing and deconstructing values of schema-described types. While `Reflect` describes the **structure** of data types, `Binding` provides the **behavior** needed to work with those types at runtime.
8
+
9
+ In other words, a binding is used to attach non-serializable Scala functions, such as **constructors**, **deconstructors**, and **matchers**, to a reflection type.
10
+
11
+ The combination of `Reflect` and `Binding` forms the foundation of `Schema`: a bound reflect (one where `F[_, _] = Binding`) that can both describe and manipulate data.
12
+
13
+ ```scala
14
+ sealed trait Binding[T <: BindingType, A]
15
+ ```
16
+
17
+ The `Binding` trait is parameterized by:
18
+
19
+ - **`T <: BindingType`**: A phantom type that identifies the kind of binding (Record, Variant, Seq, Map, etc.)
20
+ - **`A`**: The Scala type that this binding operates on
21
+
22
+ ## The Role of Binding in the Schema Architecture
23
+
24
+ ZIO Blocks separates operational behavior from structural metadata using two key abstractions:
25
+
26
+ 1. **Reflect** (`Reflect[F, A]`): A generic data structure describing the shape of type `A`. It's parameterized by `F[_, _]`, which can be plugged with different "binding strategies".
27
+ 2. **Binding**: The concrete binding strategy that embeds construction/deconstruction capabilities within a `Reflect` structure. The binding is the operational behavior attached to the reflect.
28
+
29
+ When `F[_, _] = Binding`, you get a **bound reflect** that can actually construct and deconstruct values. When `F[_, _] = NoBinding`, you get an **unbound reflect** that contains only structural metadata and is fully **serializable**.
30
+
31
+ ```scala
32
+ // Type aliases for clarity
33
+ type Reflect.Bound[A] = Reflect[Binding, A] // Can construct/deconstruct A
34
+ type Reflect.Unbound[A] = Reflect[NoBinding, A] // Pure metadata, serializable
35
+ ```
36
+
37
+ A `Schema[A]` is simply a wrapper around `Reflect.Bound[A]`:
38
+
39
+ ```scala
40
+ final class Schema[A](val reflect: Reflect.Bound[A])
41
+ ```
42
+
43
+ ## Binding Variants
44
+
45
+ The `Binding` sealed trait has several case class variants, each corresponding to a different kind of `Reflect` node:
46
+
47
+ ### 1. `Binding.Record`
48
+
49
+ Provides construction and deconstruction capabilities for product types—case classes, tuples, and any type composed of multiple named or positional fields.
50
+
51
+ ```scala
52
+ final case class Record[A](
53
+ constructor: Constructor[A],
54
+ deconstructor: Deconstructor[A],
55
+ defaultValue: Option[() => A] = None,
56
+ examples: collection.immutable.Seq[A] = Nil
57
+ ) extends Binding[BindingType.Record, A]
58
+ ```
59
+
60
+ **Components:**
61
+
62
+ - `constructor`: Builds an `A` from primitive components stored in `Registers`
63
+ - `deconstructor`: Breaks down an `A` into primitive components in `Registers`
64
+ - `defaultValue`: An optional thunk `() => A` that produces a default instance. This is useful for formats like Protocol Buffers where missing fields assume default values.
65
+ - `examples`: Sample values for documentation or testing, or OpenAPI schema generation.
66
+
67
+ Here is an example of a `Binding.Record` for a simple `Person` case class:
68
+
69
+ ```scala mdoc:compile-only
70
+ import zio.blocks.schema.binding._
71
+ import zio.blocks.schema.binding.RegisterOffset._
72
+
73
+ case class Person(name: String, age: Int)
74
+
75
+ val initialRegisters = RegisterOffset(objects = 1, ints = 1)
76
+
77
+ val personRecord: Binding.Record[Person] =
78
+ Binding.Record[Person](
79
+ constructor = new Constructor[Person] {
80
+ def usedRegisters = initialRegisters
81
+
82
+ def construct(in: Registers, offset: RegisterOffset): Person =
83
+ Person(
84
+ in.getObject(offset).asInstanceOf[String],
85
+ in.getInt(offset + RegisterOffset(objects = 1))
86
+ )
87
+ },
88
+ deconstructor = new Deconstructor[Person] {
89
+
90
+ def usedRegisters = initialRegisters
91
+
92
+ def deconstruct(out: Registers, offset: RegisterOffset, p: Person): Unit = {
93
+ out.setObject(offset, p.name)
94
+ out.setInt(offset + RegisterOffset(objects = 1), p.age)
95
+ }
96
+ }
97
+ )
98
+ ```
99
+
100
+ ### 2. `Binding.Variant`
101
+
102
+ The `Binding.Variant` data type provides discrimination capabilities and matchers for sum types—sealed traits, Scala 3 enums, and any type that represents one of several possible alternatives:
103
+
104
+ ```scala
105
+ final case class Variant[A](
106
+ discriminator: Discriminator[A],
107
+ matchers: Matchers[A],
108
+ defaultValue: Option[() => A] = None,
109
+ examples: collection.immutable.Seq[A] = Nil
110
+ ) extends Binding[BindingType.Variant, A]
111
+ ```
112
+
113
+ **Discriminator** and **Matchers** are complementary components in ZIO Blocks that work together to handle sum types (ADTs/sealed traits/enums):
114
+
115
+ - `Discriminator`: Given a value of a sum type, determine which case it belongs to by returning a numerical index.
116
+ - `Matchers`: Given a value and a case index, safely downcast the value to the specific case type, or return null if it doesn't match.
117
+
118
+ ```scala mdoc:compile-only
119
+ import zio.blocks.schema.binding._
120
+
121
+ sealed trait Shape extends Product with Serializable
122
+
123
+ case class Circle(radius: Double) extends Shape
124
+
125
+ case class Rectangle(width: Double, height: Double) extends Shape
126
+
127
+ val shapeBinding = Binding.Variant[Shape](
128
+ discriminator = new Discriminator[Shape] {
129
+ override def discriminate(a: Shape): Int = a match {
130
+ case Circle(_) => 0
131
+ case Rectangle(_, _) => 1
132
+ }
133
+ },
134
+ matchers = Matchers(
135
+ new Matcher[Circle] {
136
+ override def downcastOrNull(any: Any): Circle =
137
+ any match {
138
+ case c: Circle => c
139
+ case _ => null.asInstanceOf[Circle]
140
+ }
141
+ },
142
+ new Matcher[Rectangle] {
143
+ override def downcastOrNull(any: Any): Rectangle =
144
+ any match {
145
+ case r: Rectangle => r
146
+ case _ => null.asInstanceOf[Rectangle]
147
+ }
148
+ }
149
+ )
150
+ )
151
+ ```
152
+
153
+ Please note that the `Variant` is only responsible for discrimination and downcasting. The actual construction and deconstruction of each case is handled by the individual case's `Binding.Record` (or other appropriate binding).
154
+
155
+ ### 3. Binding.Seq
156
+
157
+ The `Binding.Seq` data type provides efficient construction and deconstruction capabilities for sequence/collection types—`List`, `Vector`, `Array`, `Set`, `Chunk`, and any ordered collection:
158
+
159
+ ```scala
160
+ final case class Seq[C[_], A](
161
+ constructor: SeqConstructor[C],
162
+ deconstructor: SeqDeconstructor[C],
163
+ defaultValue: Option[() => C[A]] = None,
164
+ examples: collection.immutable.Seq[C[A]] = Nil
165
+ ) extends Binding[BindingType.Seq[C], C[A]]
166
+ ```
167
+
168
+ The type parameter is `C[_]` (the collection type constructor) rather than `C[A]` (a specific element type). This allows a single `Binding.Seq[List]` to work for `List[Int]`, `List[String]`, `List[Person]`, etc. Inside the companion object of `Binding.Seq`, there are implementations for standard Scala collections like `List`, `Vector`, `Seq`, `IndexedSeq`, `Set`, and `ArraySeq`.
169
+
170
+ Sequences are ubiquitous in data modeling, and their efficient handling is critical for performance. The `Seq` binding abstracts over the specific collection type using the **builder pattern**, enabling both stacked and heap-allocated construction strategies.
171
+
172
+ - `seqConstructor` is a sequence constructor which provides how to build the sequence of type `C[_]`. It provides collection-specific builder operations. Different collections have different constructor strategies (e.g., `Array` needs size upfront and is stack-allocated, while `List` can be built incrementally on the heap).
173
+ - `seqDeconstructor` is a sequence deconstructor that provides iteration and size information for the collection type `C[_]`, which enables us to tear down the collection into its elements efficiently. The size hints enable codecs to pre-allocate output buffers and builders.
174
+
175
+ Let's take a deep dive into these two main components.
176
+
177
+ #### SeqConstructor
178
+
179
+ The `SeqConstructor` has two main variants:
180
+
181
+ 1. **Heap-allocated Constructors** which are for collections like `List`, `Vector`, `Set`, etc. These collections typically use mutable builders under the hood to efficiently accumulate elements before producing an immutable collection. The `Boxed` abstract class is a convenient base class for heap-allocated constructors that uses a single builder type for all primitive types. This makes it easy to implement sequence constructors for collections of heap-allocated objects:
182
+
183
+ ```scala
184
+ abstract class Boxed[C[_]] extends SeqConstructor[C] {
185
+ override type BooleanBuilder = ObjectBuilder[Boolean]
186
+ override type ByteBuilder = ObjectBuilder[Byte]
187
+ // ... all primitive builders are aliases to ObjectBuilder
188
+
189
+ def addBoolean(builder: BooleanBuilder, a: Boolean): Unit = addObject(builder, a)
190
+ // primitives get boxed when passed to addObject
191
+
192
+ // ... similarly for other primitive adders
193
+ }
194
+ ```
195
+
196
+ To implement a boxed sequence, we have to define just four things: the builder type, how to create a new builder, how to add elements to it, and how to produce the final collection.
197
+
198
+ ZIO Blocks provides `SeqConstructor.Boxed` implementations for standard Scala collections like `List`, `Vector`, `Set`, `IndexedSeq` and `Seq`. Here is an example of a `SeqConstructor.Boxed` for `List`:
199
+
200
+ ```scala
201
+ val listConstructor: SeqConstructor[List] = new Boxed[List] {
202
+ type ObjectBuilder[A] = scala.collection.mutable.ListBuffer[A]
203
+
204
+ def newObjectBuilder[A](sizeHint: Int): ObjectBuilder[A] = new ListBuffer[A]
205
+
206
+ def addObject[A](builder: ObjectBuilder[A], a: A): Unit = builder.addOne(a)
207
+
208
+ def resultObject[A](builder: ObjectBuilder[A]): List[A] = builder.toList
209
+ }
210
+ ```
211
+
212
+ 2. **Stack-allocated Constructors** which are for collections like `Array`, `ArraySeq`, and `IArray`.
213
+
214
+ All the implementations are found in the companion object of the `SeqConstructor` trait.
215
+
216
+ #### SeqDeconstructor
217
+
218
+ The `SeqDeconstructor` has a simple interface with two methods:
219
+
220
+ ```scala
221
+ trait SeqDeconstructor[C[_]] {
222
+ def deconstruct[A](c: C[A]): Iterator[A]
223
+
224
+ def size[A](c: C[A]): Int
225
+ }
226
+ ```
227
+
228
+ The `deconstruct` method provides an iterator over the elements of the collection, while the `size` method returns the number of elements.
229
+
230
+ Inside the companion object of the `SeqDeconstructor` there are implementations for standard Scala collections like `List`, `Vector`, `Seq`, and `IndexedSeq`.
231
+
232
+ For efficiency, there are specialized implementations for array-based sequences like `Array`, `ArraySeq`, and `IArray` which provide direct access to elements, besides the iterator-based approach. As a result, they are much more performant than the generic iterator-based deconstruction. When an `Optic` needs to access a specific element by index, it can use these specialized methods to avoid the overhead of iterator traversal.
233
+
234
+ This is done through the `SpecializedIndexed` trait:
235
+
236
+ ```scala
237
+ sealed trait SpecializedIndexed[C[_]] extends SeqDeconstructor[C] {
238
+ def elementType[A](c: C[A]): RegisterType[A]
239
+ def objectAt[A](c: C[A], index: Int): A
240
+ def booleanAt(c: C[Boolean], index: Int): Boolean
241
+ def byteAt(c: C[Byte], index: Int): Byte
242
+ def shortAt(c: C[Short], index: Int): Short
243
+ def intAt(c: C[Int], index: Int): Int
244
+ def longAt(c: C[Long], index: Int): Long
245
+ def floatAt(c: C[Float], index: Int): Float
246
+ def doubleAt(c: C[Double], index: Int): Double
247
+ def charAt(c: C[Char], index: Int): Char
248
+ }
249
+ ```
250
+
251
+ You can find implementations of `SpecializedIndexed` for `Array`, `ArraySeq`, and `IArray` in the companion object of `SeqDeconstructor`.
252
+
253
+ ### 4. Binding.Map
254
+
255
+ To describe the construction and deconstruction of key-value collections, use `Binding.Map`:
256
+
257
+ ```scala
258
+ final case class Map[M[_, _], K, V](
259
+ constructor: MapConstructor[M],
260
+ deconstructor: MapDeconstructor[M],
261
+ defaultValue: Option[() => M[K, V]] = None,
262
+ examples: collection.immutable.Seq[M[K, V]] = Nil
263
+ ) extends Binding[BindingType.Map[M], M[K, V]]
264
+ ```
265
+
266
+ It has two main components for building maps and tearing down maps to their key-value pairs:
267
+
268
+ - `mapConstructor`: Builds maps from key-value pairs using builders
269
+ - `mapDeconstructor`: Provides key-value iteration for maps
270
+
271
+ Their interfaces are similar to `SeqConstructor` and `SeqDeconstructor`, but specialized for key-value pairs.
272
+
273
+ ZIO Blocks has an implementation of `Binding.Map` for standard Scala `Map`s in the companion object of `Binding.Map` called `Binding.Map.map`.
274
+
275
+ ### 5. Binding.Primitive
276
+
277
+ The `Binding.Primitive` provides metadata for primitive/scalar types—`Int`, `Long`, `Double`, `String`, `Boolean`, `Byte`, `Short`, `Char`, `Float`, `BigInt`, `BigDecimal`, `UUID`, and temporal types.
278
+
279
+ ```scala
280
+ final case class Primitive[A](
281
+ defaultValue: Option[() => A] = None,
282
+ examples: collection.immutable.Seq[A] = Nil
283
+ ) extends Binding[BindingType.Primitive, A]
284
+ ```
285
+
286
+ Primitives require minimal binding logic since they are atomic values that don't need construction from parts. You can access all built-in primitive bindings in the companion object of `Binding.Primitive`, such as `Primitive.int`, `Primitive.string`, etc.
287
+
288
+ ### 6. Binding.Wrapper
289
+
290
+ The `Binding.Wrapper` provides wrap/unwrap capabilities for newtype patterns—opaque types, value classes, validated wrappers, and single-field case class wrappers.
291
+
292
+ `Newtypes` are extremely common in well-designed codebases. Instead of passing raw String values for emails, you define `Email` as a distinct type. Instead of `Int` for user IDs, you define `UserId`. This provides type safety without runtime overhead.
293
+
294
+ The Wrapper binding handles the bidirectional transformation between the wrapper type and its underlying representation, with optional validation:
295
+
296
+ ```scala
297
+ final case class Wrapper[A, B](
298
+ wrap: B => Either[SchemaError, A],
299
+ unwrap: A => Either[SchemaError, B]
300
+ ) extends Binding[BindingType.Wrapper[A, B], A]
301
+ ```
302
+
303
+ **Components:**
304
+
305
+ - `wrap`: Converts from the underlying type `B` to the wrapper type `A`, returning `Right(a)` on success or `Left(SchemaError)` on failure
306
+ - `unwrap`: Extracts the underlying `B` from an `A`, returning `Right(b)` on success or `Left(SchemaError)` on failure
307
+
308
+ Here is an example of a `Binding.Wrapper` for an `Email` newtype with validation:
309
+
310
+ ```scala mdoc:compile-only
311
+ import zio.blocks.schema._
312
+ import zio.blocks.schema.binding._
313
+
314
+ case class Email(value: String)
315
+
316
+ object Email {
317
+ private val EmailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$".r
318
+ new Binding.Wrapper[Email, String](
319
+ wrap = {
320
+ case x@EmailRegex(_*) => Right(new Email(x))
321
+ case _ => Left(SchemaError.validationFailed("Expected valid email format"))
322
+ },
323
+ email => Right(email.value)
324
+ )
325
+ }
326
+ ```
327
+
328
+ ### 7. Binding.Dynamic
329
+
330
+ To bind untyped dynamic values, use `Binding.Dynamic`. It provides a binding for dynamically typed values whose structure is not known at compile time:
331
+
332
+ ```scala
333
+ final case class Dynamic(
334
+ defaultValue: Option[() => DynamicValue] = None,
335
+ examples: collection.immutable.Seq[DynamicValue] = Nil
336
+ ) extends Binding[BindingType.Dynamic, DynamicValue]
337
+ ```
338
+
339
+ Used when the schema of the data is not known at compile time, such as JSON payloads with arbitrary structure.
340
+
341
+ ## Binding and Schema Serialization
342
+
343
+ When `F[_, _] = NoBinding` in `Reflect[F[_, _], A]` the `Reflect` structure contains only pure data (no functions) and becomes fully serializable. This enables:
344
+
345
+ 1. **Schema serialization**: Convert schemas to JSON Schema or other formats, making them portable
346
+ 2. **Schema rebinding**: Deserialize a schema and rebind it using a `TypeRegistry`, so it becomes type-safe and operational again
347
+
348
+ We will cover schema serialization and rebinding in more detail in the `Reflect` data type documentation page.
349
+
350
+ ## Summary
351
+
352
+ The `Binding` data type is the operational heart of ZIO Blocks' schema system:
353
+
354
+ | Reflect Node | Binding Variant | Construction and Deconstruction of |
355
+ |---------------------|---------------------|------------------------------------|
356
+ | `Reflect.Record` | `Binding.Record` | Product Types |
357
+ | `Reflect.Variant` | `Binding.Variant` | Sum Types |
358
+ | `Reflect.Sequence` | `Binding.Seq` | Sequence Collection Types |
359
+ | `Reflect.Map` | `Binding.Map` | Key-value Collection Types |
360
+ | `Reflect.Primitive` | `Binding.Primitive` | Primitive Types |
361
+ | `Reflect.Wrapper` | `Binding.Wrapper` | Wrap/unwrap newtypes |
362
+ | `Reflect.Dynamic` | `Binding.Dynamic` | Untyped Dynamic Values |
363
+
364
+ A key innovation is the clean separation between structure (`Reflect`) and behavior (`Binding`), enabling us to achieve serializable schemas with unbound reflects, while the `Reflect` remains pluggable with the `Binding` for runtime operations.