@zio.dev/zio-blocks 0.0.21 → 0.0.24

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,1960 @@
1
+ ---
2
+ id: type-class-derivation
3
+ title: "Type Class Derivation"
4
+ ---
5
+
6
+ Type classes are one of the most powerful abstraction mechanisms in functional programming. Originating from Haskell, they enable ad-hoc polymorphism—the ability to define generic behavior that can be extended to new types without modifying those types. ZIO Blocks has a robust type class derivation system built around the `Deriver` trait, which allows automatic generation of type class instances for any data type with an associated `Schema`.
7
+
8
+ The `Deriver` trait is a cornerstone of ZIO Blocks' type class derivation system. It provides a unified, elegant mechanism for automatically generating type class instances (such as codecs) for any data type that has a `Schema`. Unlike traditional macro-based derivation approaches, `Deriver` requires implementing only a few methods to enable full type class derivation with rich reflective metadata support for every use case.
9
+
10
+ ## The Problem
11
+
12
+ In functional programming, type classes allow us to define generic behavior that can be extended to new types without modifying those types. However, manually writing type class instances for every data type can be tedious and error-prone, especially as the number of types grows. This is where automatic derivation comes in.
13
+
14
+ Consider a typical application with 50 domain types that needs 4 type classes (JSON codec, Avro codec, hashing, ordering). That's 200 type class instances to write and maintain manually (50 types × 4 type classes).
15
+
16
+ Each instance requires understanding both the type's structure and the type class's semantics, then correctly implementing encoding, decoding, or whatever operation is required. This quickly becomes unmanageable as the codebase grows.
17
+
18
+ Assume we have a simple `JsonCodec` type class for JSON serialization and deserialization:
19
+
20
+ ```scala
21
+ import zio.blocks.schema.json._
22
+
23
+ sealed abstract class JsonError(msg: String) extends Exception(msg)
24
+
25
+ case class ParseError(details: String)
26
+ extends JsonError(s"Parse Error: $details")
27
+
28
+ case class DecodeError(details: String, path: String)
29
+ extends JsonError(s"Decode Error at '$path': $details")
30
+
31
+ trait JsonCodec[A] {
32
+ def encode(a: A): Json
33
+ def decode(j: Json): Either[JsonError, A]
34
+ }
35
+ ```
36
+
37
+ A single manual codec for a simple type like `Person` looks like the following code. You can imagine how complex it gets for larger types and more type classes:
38
+
39
+ ```scala
40
+ case class Person(name: String, age: Int)
41
+
42
+ object Person {
43
+ implicit val personCodec: JsonCodec[Person] =
44
+ new JsonCodec[Person] {
45
+ def encode(c: Person): Json = Json.obj(
46
+ "name" -> Json.str(c.name),
47
+ "age" -> Json.number(c.age)
48
+ )
49
+
50
+ def decode(j: Json): Either[JsonError, Person] =
51
+ for {
52
+ name <- j.get("name").asString.string
53
+ age <- j.get("age").asNumber.int
54
+ } yield Person(name, age)
55
+ }
56
+ }
57
+ ```
58
+
59
+ This manual approach is not only time-consuming but also prone to errors and inconsistencies. As the number of types and type classes increases, the maintenance burden grows significantly.
60
+
61
+ ## The Solution: Automatic Derivation with `Deriver`
62
+
63
+ The `Deriver` trait provides a powerful and flexible way to automatically derive type class instances for any data type with an associated `Schema`. By implementing just seven methods, you can enable full derivation for primitive types, records, variants, sequences, maps, dynamic values, and wrappers.
64
+
65
+ ZIO Blocks recognizes that all data types reduce to a small set of structural patterns (as outlined in the `Reflect` documentation):
66
+
67
+ | Pattern | Description | Examples |
68
+ |---------------|---------------------------------|------------------------------------|
69
+ | **Primitive** | Atomic values | `String`, `Int`, `UUID`, `Instant` |
70
+ | **Record** | Product types with named fields | Case classes, tuples |
71
+ | **Variant** | Sum types with named cases | Sealed traits, enums |
72
+ | **Sequence** | Ordered collections | `List`, `Vector`, `Array` |
73
+ | **Map** | Key-value collections | `Map`, `HashMap` |
74
+ | **Dynamic** | Schema-less data | `DynamicValue`, arbitrary JSON |
75
+ | **Wrapper** | Newtypes and opaque types | `opaque type Age = Int` |
76
+
77
+ If you define how to derive type-class instances for all these patterns, then ZIO Blocks has all the pieces needed to build type-class instances for any data type. This is what the `Deriver[TC[_]]` is responsible for. A `Deriver[TC[_]]` defines how to create `TC[A]` instances for each kind of schema node:
78
+
79
+ ```scala
80
+ trait Deriver[TC[_]] {
81
+ def derivePrimitive[A](...) : Lazy[TC[A]]
82
+ def deriveRecord [F[_, _], A](...) : Lazy[TC[A]]
83
+ def deriveVariant [F[_, _], A](...) : Lazy[TC[A]]
84
+ def deriveSequence [F[_, _], C[_], A](...) : Lazy[TC[C[A]]]
85
+ def deriveMap [F[_, _], M[_, _], K, V](...): Lazy[TC[M[K, V]]]
86
+ def deriveDynamic [F[_, _]](...) : Lazy[TC[DynamicValue]]
87
+ def deriveWrapper [F[_, _], A, B](...) : Lazy[TC[A]]
88
+ }
89
+ ```
90
+
91
+ Conceptually, the `Deriver` interface operates at the meta level, acting as a type class for type class derivation. It takes a higher-kinded type parameter `TC[_]`, which represents the type class to be derived (e.g., `JsonCodec`, `Ordering`, `Eq`, etc.), and defines seven methods, each corresponding to the derivation of the type class for one of the structural patterns.
92
+
93
+ That's it. As a developer who wants to implement automatic derivation for a new type class, you only need to implement these 7 methods. Each receives all the information needed to build a type class instance such as field names, type names, bindings for construction/deconstruction, documentation, and modifiers.
94
+
95
+ Looking at the return type of each method, you'll notice they all return the type class wrapped in a `Lazy` container, i.e., `Lazy[TC[_]]`, not just `TC[_]`. This is crucial for handling recursive data types safely. While the `Deriver` system traverses the schema structure to generate type-class instances or codecs, it may encounter recursive data types. To prevent stack overflows caused by unbounded recursion and infinite loops, ZIO Blocks uses the `Lazy` data type, which is a trampolined, memoizing lazy evaluation monad that defers computation until `Lazy#force` is called. It provides stack-safe evaluation through continuation-passing style (CPS), along with error-handling capabilities and composable operations.
96
+
97
+ Each method (except the `derivePrimitive` method) also receives implicit parameters of type class instances for `HasBinding` and `HasInstance`:
98
+ 1. **`HasBinding[F]`**: Provides access to the structural binding information (constructors, deconstructors, matchers, discriminators, etc.) for the contained types, e.g., fields of a record or cases of a variant, allowing us to understand how to construct and deconstruct values of those types.
99
+ 2. **`HasInstance[F, TC]`**: Provides access to already-(provided/derived) type class instances for nested types or fields. This allows you to build type class instances for complex types by composing instances of their constituent parts.
100
+
101
+ As an example, the `deriveRecord` method signature looks like this:
102
+
103
+ ```scala
104
+ trait Deriver[TC[_]] {
105
+ // other methods...
106
+
107
+ def deriveRecord[F[_, _], A](
108
+ fields: IndexedSeq[Term[F, A, ?]],
109
+ typeId: TypeId[A],
110
+ binding: Binding[BindingType.Record, A],
111
+ doc: Doc,
112
+ modifiers: Seq[Modifier.Reflect],
113
+ defaultValue: Option[A],
114
+ examples: Seq[A]
115
+ )(implicit F: HasBinding[F], D: HasInstance[F]): Lazy[TC[A]]
116
+
117
+ // other methods...
118
+ }
119
+ ```
120
+
121
+ The other methods follow a similar pattern, each tailored to the specific structural pattern they handle.
122
+
123
+ The underlying derivation engine takes care of traversing the schema structure, applying the appropriate derivation method for each structural pattern, and composing the resulting type class instances together. This means that once you've implemented a `Deriver` for a specific type class, you can automatically derive instances for any data type with a schema, without writing any additional boilerplate code.
124
+
125
+ ## Using the `Deriver` to Derive Type Class Instances
126
+
127
+ Given a `Schema[A]`, you can call the `derive` method to get an instance of the type class `TC[A]`:
128
+
129
+ ```scala
130
+ case class Schema[A](reflect: Reflect.Bound[A]) {
131
+ def derive[TC[_]](deriver: Deriver[TC]): TC[A] = ???
132
+ }
133
+ ```
134
+
135
+ It takes a `Deriver[TC]` as a parameter and returns a type class instance of type `TC[A]`. For example, in the following code snippet, we derive a `JsonBinaryCodec[Person]` instance for the `Person` case class using the `JsonBinaryCodecDeriver`:
136
+
137
+ ```scala
138
+ import zio.blocks.schema._
139
+ import zio.blocks.schema.json.JsonBinaryCodecDeriver
140
+
141
+ case class Person(name: String, age: Int)
142
+
143
+ object Person {
144
+ implicit val schema: Schema[Person] = Schema.derived[Person]
145
+ }
146
+
147
+ val jsonCodec: JsonBinaryCodec[Person] =
148
+ Person.schema.derive(JsonBinaryCodecDeriver)
149
+
150
+ val result: Either[SchemaError, Person] =
151
+ jsonCodec.decode(
152
+ """
153
+ |{
154
+ | "name": "Alice",
155
+ | "age": 30
156
+ |}
157
+ |""".stripMargin
158
+ )
159
+ ```
160
+
161
+ There is another overloaded version of the `Schema#derive` method that takes a `Format` instead of a `Deriver`:
162
+
163
+ ```scala
164
+ case class Schema[A](reflect: Reflect.Bound[A]) {
165
+ def derive[F <: codec.Format](format: F): format.TypeClass[A] = derive(format.deriver)
166
+ }
167
+ ```
168
+
169
+ For example, by calling `Person.schema.derive(JsonFormat)`, we can derive a `JsonCodec[Person]` instance:
170
+
171
+ ```scala
172
+ import zio.blocks.schema.json._
173
+
174
+ val jsonCodec = Person.schema.derive(JsonFormat)
175
+ ```
176
+
177
+ ## Example 1: Deriving a `Show` Type Class Instance
178
+
179
+ Let's say we want to derive a `Show` type class instance for any type of type `A`:
180
+
181
+ ```scala
182
+ trait Show[A] {
183
+ def show(value: A): String
184
+ }
185
+ ```
186
+
187
+ The implementation of the `Deriver[Show]` would look like the following code. Don't worry about understanding every detail right now; we'll break down the derivation process step by step afterward.
188
+
189
+ ```scala
190
+ import zio.blocks.chunk.Chunk
191
+ import zio.blocks.schema.*
192
+ import zio.blocks.schema.DynamicValue.Null
193
+ import zio.blocks.schema.binding.*
194
+ import zio.blocks.schema.derive.Deriver
195
+ import zio.blocks.typeid.TypeId
196
+ import zio.blocks.docs.Doc
197
+
198
+ object DeriveShow extends Deriver[Show] {
199
+
200
+ override def derivePrimitive[A](
201
+ primitiveType: PrimitiveType[A],
202
+ typeId: TypeId[A],
203
+ binding: Binding[BindingType.Primitive, A],
204
+ doc: Doc,
205
+ modifiers: Seq[Modifier.Reflect],
206
+ defaultValue: Option[A],
207
+ examples: Seq[A]
208
+ ): Lazy[Show[A]] =
209
+ Lazy {
210
+ new Show[A] {
211
+ def show(value: A): String = primitiveType match {
212
+ case _: PrimitiveType.String => "\"" + value + "\""
213
+ case _: PrimitiveType.Char => "'" + value + "'"
214
+ case _ => String.valueOf(value)
215
+ }
216
+ }
217
+ }
218
+
219
+ override def deriveRecord[F[_, _], A](
220
+ fields: IndexedSeq[Term[F, A, ?]],
221
+ typeId: TypeId[A],
222
+ binding: Binding[BindingType.Record, A],
223
+ doc: Doc,
224
+ modifiers: Seq[Modifier.Reflect],
225
+ defaultValue: Option[A],
226
+ examples: Seq[A]
227
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[A]] =
228
+ Lazy {
229
+ // Collecting Lazy[Show] instances for each field from the transformed metadata
230
+ val fieldShowInstances: IndexedSeq[(String, Lazy[Show[Any]])] = fields.map { field =>
231
+ val fieldName = field.name
232
+ // Get the Lazy[Show] instance for this field's type, but we won't force it yet
233
+ // We'll force it later when we actually need to show a value of this field
234
+ val fieldShowInstance = D.instance(field.value.metadata).asInstanceOf[Lazy[Show[Any]]]
235
+ (fieldName, fieldShowInstance)
236
+ }
237
+
238
+ // Cast fields to use Binding as F (we are going to create Reflect.Record with Binding as F)
239
+ val recordFields = fields.asInstanceOf[IndexedSeq[Term[Binding, A, ?]]]
240
+
241
+ // Cast to Binding.Record to access constructor/deconstructor
242
+ val recordBinding = binding.asInstanceOf[Binding.Record[A]]
243
+
244
+ // Build a Reflect.Record to get access to the computed registers for each field
245
+ val recordReflect = new Reflect.Record[Binding, A](recordFields, typeId, recordBinding, doc, modifiers)
246
+
247
+ new Show[A] {
248
+ def show(value: A): String = {
249
+
250
+ // Create registers with space for all used registers to hold deconstructed field values
251
+ val registers = Registers(recordReflect.usedRegisters)
252
+
253
+ // Deconstruct field values of the record into the registers
254
+ recordBinding.deconstructor.deconstruct(registers, RegisterOffset.Zero, value)
255
+
256
+ // Build string representations for all fields
257
+ val fieldStrings = fields.indices.map { i =>
258
+ val (fieldName, showInstanceLazy) = fieldShowInstances(i)
259
+ val fieldValue = recordReflect.registers(i).get(registers, RegisterOffset.Zero)
260
+ val result = s"$fieldName = ${showInstanceLazy.force.show(fieldValue)}"
261
+ result
262
+ }
263
+
264
+ s"${typeId.name}(${fieldStrings.mkString(", ")})"
265
+ }
266
+ }
267
+ }
268
+
269
+ override def deriveVariant[F[_, _], A](
270
+ cases: IndexedSeq[Term[F, A, ?]],
271
+ typeId: TypeId[A],
272
+ binding: Binding[BindingType.Variant, A],
273
+ doc: Doc,
274
+ modifiers: Seq[Modifier.Reflect],
275
+ defaultValue: Option[A],
276
+ examples: Seq[A]
277
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[A]] = Lazy {
278
+ // Get Show instances for all cases LAZILY
279
+ val caseShowInstances: IndexedSeq[Lazy[Show[Any]]] = cases.map { case_ =>
280
+ D.instance(case_.value.metadata).asInstanceOf[Lazy[Show[Any]]]
281
+ }
282
+
283
+ // Cast binding to Binding.Variant to access discriminator and matchers
284
+ val variantBinding = binding.asInstanceOf[Binding.Variant[A]]
285
+ val discriminator = variantBinding.discriminator
286
+ val matchers = variantBinding.matchers
287
+
288
+ new Show[A] {
289
+ // Implement show by using discriminator and matchers to find the right case
290
+ // The `value` parameter is of type A (the variant type), e.g. an Option[Int] value
291
+ def show(value: A): String = {
292
+ // Use discriminator to determine which case this value belongs to
293
+ val caseIndex = discriminator.discriminate(value)
294
+
295
+ // Use matcher to downcast to the specific case type
296
+ val caseValue = matchers(caseIndex).downcastOrNull(value)
297
+
298
+ // Just delegate to the case's Show instance - it already knows its own name
299
+ caseShowInstances(caseIndex).force.show(caseValue)
300
+ }
301
+ }
302
+ }
303
+
304
+ override def deriveSequence[F[_, _], C[_], A](
305
+ element: Reflect[F, A],
306
+ typeId: TypeId[C[A]],
307
+ binding: Binding[BindingType.Seq[C], C[A]],
308
+ doc: Doc,
309
+ modifiers: Seq[Modifier.Reflect],
310
+ defaultValue: Option[C[A]],
311
+ examples: Seq[C[A]]
312
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[C[A]]] = Lazy {
313
+ // Get Show instance for element type LAZILY
314
+ val elementShowLazy: Lazy[Show[A]] = D.instance(element.metadata)
315
+
316
+ // Cast binding to Binding.Seq to access the deconstructor
317
+ val seqBinding = binding.asInstanceOf[Binding.Seq[C, A]]
318
+ val deconstructor = seqBinding.deconstructor
319
+
320
+ new Show[C[A]] {
321
+ def show(value: C[A]): String = {
322
+ // Use deconstructor to iterate over elements
323
+ val iterator = deconstructor.deconstruct(value)
324
+ // Force the element Show instance only when actually showing
325
+ val elements = iterator.map(elem => elementShowLazy.force.show(elem)).mkString(", ")
326
+ s"[$elements]"
327
+ }
328
+ }
329
+ }
330
+
331
+ override def deriveMap[F[_, _], M[_, _], K, V](
332
+ key: Reflect[F, K],
333
+ value: Reflect[F, V],
334
+ typeId: TypeId[M[K, V]],
335
+ binding: Binding[BindingType.Map[M], M[K, V]],
336
+ doc: Doc,
337
+ modifiers: Seq[Modifier.Reflect],
338
+ defaultValue: Option[M[K, V]],
339
+ examples: Seq[M[K, V]]
340
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[M[K, V]]] = Lazy {
341
+ // Get Show instances for key and value types LAZILY
342
+ val keyShowLazy: Lazy[Show[K]] = D.instance(key.metadata)
343
+ val valueShowLazy: Lazy[Show[V]] = D.instance(value.metadata)
344
+
345
+ // Cast binding to Binding.Map to access the deconstructor
346
+ val mapBinding = binding.asInstanceOf[Binding.Map[M, K, V]]
347
+ val deconstructor = mapBinding.deconstructor
348
+
349
+ new Show[M[K, V]] {
350
+ def show(m: M[K, V]): String = {
351
+ // Use deconstructor to iterate over key-value pairs
352
+ val iterator = deconstructor.deconstruct(m)
353
+ // Force the Show instances only when actually showing
354
+ val entries = iterator.map { kv =>
355
+ val k = deconstructor.getKey(kv)
356
+ val v = deconstructor.getValue(kv)
357
+ s"${keyShowLazy.force.show(k)} -> ${valueShowLazy.force.show(v)}"
358
+ }.mkString(", ")
359
+ s"Map($entries)"
360
+ }
361
+ }
362
+ }
363
+
364
+ override def deriveDynamic[F[_, _]](
365
+ binding: Binding[BindingType.Dynamic, DynamicValue],
366
+ doc: Doc,
367
+ modifiers: Seq[Modifier.Reflect],
368
+ defaultValue: Option[DynamicValue],
369
+ examples: Seq[DynamicValue]
370
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[DynamicValue]] = Lazy {
371
+ new Show[DynamicValue] {
372
+ def show(value: DynamicValue): String =
373
+ value match {
374
+ case DynamicValue.Primitive(pv) =>
375
+ value.toString
376
+
377
+ case DynamicValue.Record(fields) =>
378
+ val fieldStrings = fields.map { case (name, v) =>
379
+ s"$name = ${show(v)}"
380
+ }
381
+ s"Record(${fieldStrings.mkString(", ")})"
382
+
383
+ case DynamicValue.Variant(caseName, v) =>
384
+ s"$caseName(${show(v)})"
385
+
386
+ case DynamicValue.Sequence(elements) =>
387
+ val elemStrings = elements.map(show)
388
+ s"[${elemStrings.mkString(", ")}]"
389
+
390
+ case DynamicValue.Map(entries) =>
391
+ val entryStrings = entries.map { case (k, v) =>
392
+ s"${show(k)} -> ${show(v)}"
393
+ }
394
+ s"Map(${entryStrings.mkString(", ")})"
395
+ case Null =>
396
+ "null"
397
+ }
398
+ }
399
+ }
400
+
401
+ override def deriveWrapper[F[_, _], A, B](
402
+ wrapped: Reflect[F, B],
403
+ typeId: TypeId[A],
404
+ binding: Binding[BindingType.Wrapper[A, B], A],
405
+ doc: Doc,
406
+ modifiers: Seq[Modifier.Reflect],
407
+ defaultValue: Option[A],
408
+ examples: Seq[A]
409
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[A]] = Lazy {
410
+ // Get Show instance for the wrapped (underlying) type B LAZILY
411
+ val wrappedShowLazy: Lazy[Show[B]] = D.instance(wrapped.metadata)
412
+
413
+ // Cast binding to Binding.Wrapper to access unwrap function
414
+ val wrapperBinding = binding.asInstanceOf[Binding.Wrapper[A, B]]
415
+
416
+ new Show[A] {
417
+ def show(value: A): String = {
418
+ val unwrapped = wrapperBinding.unwrap(value)
419
+ s"${typeId.name}(${wrappedShowLazy.force.show(unwrapped)})"
420
+ }
421
+ }
422
+ }
423
+ }
424
+ ```
425
+
426
+ Now let's see how the derivation process works step by step.
427
+
428
+ ### Primitive Derivation
429
+
430
+ When the derivation process encounters a primitive type (e.g., `String`, `Int`), it calls the `derivePrimitive` method of the `Deriver`. This method receives the `PrimitiveType[A]` information, which allows it to determine how to encode and decode values of that type:
431
+
432
+
433
+ ```scala
434
+ def derivePrimitive[A](
435
+ primitiveType: PrimitiveType[A],
436
+ typeId: TypeId[A],
437
+ binding: Binding[BindingType.Primitive, A],
438
+ doc: Doc,
439
+ modifiers: Seq[Modifier.Reflect],
440
+ defaultValue: Option[A],
441
+ examples: Seq[A]
442
+ ): Lazy[Show[A]] =
443
+ Lazy {
444
+ new Show[A] {
445
+ def show(value: A): String = primitiveType match {
446
+ case _: PrimitiveType.String => "\"" + value + "\""
447
+ case _: PrimitiveType.Char => "'" + value + "'"
448
+ case _ => String.valueOf(value)
449
+ }
450
+ }
451
+ }
452
+ ```
453
+
454
+ Please note that for our simple `Show` type class, we only need to know the `PrimitiveType` to determine how to show the value. However, for more complex type classes you might require additional information from the other parameters (e.g., documentation, modifiers, default values, examples) to build a more sophisticated type class instance.
455
+
456
+ To make it simple, we only handle `String` and `Char` differently by adding quotes around them, while for all other primitive types we simply call `String.valueOf(value)` to get their string representation. You can easily extend this logic to handle other primitive types differently if needed.
457
+
458
+ ### Record Derivation
459
+
460
+ When the derivation process encounters a record type (e.g., a case class), it calls the `deriveRecord` method of the `Deriver`. This method receives an `IndexedSeq[Term[F, A, ?]]` representing the fields of the record, along with other metadata such as the type ID, binding information, documentation, modifiers, default values, and examples. It also receives implicit parameters for accessing structural bindings and already-derived type class instances for nested types:
461
+
462
+
463
+ ```scala
464
+ def deriveRecord[F[_, _], A](
465
+ fields: IndexedSeq[Term[F, A, ?]],
466
+ typeId: TypeId[A],
467
+ binding: Binding[BindingType.Record, A],
468
+ doc: Doc,
469
+ modifiers: Seq[Modifier.Reflect],
470
+ defaultValue: Option[A],
471
+ examples: Seq[A]
472
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[A]] =
473
+ Lazy {
474
+ // Collecting Lazy[Show] instances for each field from the transformed metadata
475
+ val fieldShowInstances: IndexedSeq[(String, Lazy[Show[Any]])] = fields.map { field =>
476
+ val fieldName = field.name
477
+ // Get the Lazy[Show] instance for this field's type, but we won't force it yet
478
+ // We'll force it later when we actually need to show a value of this field
479
+ val fieldShowInstance = D.instance(field.value.metadata).asInstanceOf[Lazy[Show[Any]]]
480
+ (fieldName, fieldShowInstance)
481
+ }
482
+
483
+ // Cast fields to use Binding as F (we are going to create Reflect.Record with Binding as F)
484
+ val recordFields = fields.asInstanceOf[IndexedSeq[Term[Binding, A, ?]]]
485
+
486
+ // Cast to Binding.Record to access constructor/deconstructor
487
+ val recordBinding = binding.asInstanceOf[Binding.Record[A]]
488
+
489
+ // Build a Reflect.Record to get access to the computed registers for each field
490
+ val recordReflect = new Reflect.Record[Binding, A](recordFields, typeId, recordBinding, doc, modifiers)
491
+
492
+ new Show[A] {
493
+ def show(value: A): String = {
494
+
495
+ // Create registers with space for all used registers to hold deconstructed field values
496
+ val registers = Registers(recordReflect.usedRegisters)
497
+
498
+ // Deconstruct field values of the record into the registers
499
+ recordBinding.deconstructor.deconstruct(registers, RegisterOffset.Zero, value)
500
+
501
+ // Build string representations for all fields
502
+ val fieldStrings = fields.indices.map { i =>
503
+ val (fieldName, showInstanceLazy) = fieldShowInstances(i)
504
+ val fieldValue = recordReflect.registers(i).get(registers, RegisterOffset.Zero)
505
+ val result = s"$fieldName = ${showInstanceLazy.force.show(fieldValue)}"
506
+ result
507
+ }
508
+
509
+ s"${typeId.name}(${fieldStrings.mkString(", ")})"
510
+ }
511
+ }
512
+ }
513
+ ```
514
+
515
+ The `deriveRecord` method demonstrates derivation mechanics for record types such as case classes and tuples. To derive the type class for a record type, we follow these steps:
516
+ 1. First, we extract the type class instances for each field of the record.
517
+ 2. Second, we have to deconstruct the record value at runtime to access individual field values.
518
+ 3. Third, we assemble the final string representation of the record by combining field names and their corresponding representations using the extracted type class instances.
519
+
520
+ During the first step, the method gathers `Lazy[Show]` instances for each field by calling `D.instance(field.value.metadata)`. This method extracts the derived type class instance for the field's type from the transformed schema metadata. Again, the transformed metadata contains `Reflect[BindingInstance[TC, _, _], A]` nodes, where each node has a `BindingInstance` that bundles together the structural binding and the derived type class instance. By calling `D.instance`, we retrieve the `Lazy[Show]` instance for each field's type.
521
+
522
+ These instances are wrapped in `Lazy` to support recursive data types—if a `Person` contains a `List[Person]`, we need to delay forcing the inner `Show[Person]` until runtime to avoid infinite loops during derivation.
523
+
524
+ Our goal is to build a `String` representation of the record in the format `TypeName(field1 = value1, field2 = value2, ...)`. To achieve this, we need to access the individual field values of the record at runtime. To do this, we have to deconstruct the record value, which is given to the `show(value: A)` method, into its individual fields.
525
+
526
+ To deconstruct the record, we use the `Binding.Record[A]` that was provided as a parameter to the `deriveRecord` method. This binding contains a `deconstructor` that knows how to extract all field values from a record of type `A`. To perform the deconstruction, we should first allocate register buffers to hold the deconstructed field values. But how do we know what the size of the register buffer should be? This is where the `Reflect.Record` comes in. By building a `Reflect.Record[Binding, A]` from the field definitions, we can compute the number of registers needed to hold all field values through `Reflect#usedRegisters`. The `Registers(recordReflect.usedRegisters)` call allocates a register buffer with the appropriate size to hold all field values of the record.
527
+
528
+ Now we are ready to deconstruct the `A` value, using the `Binding.Record#deconstructor.deconstruct(registers, RegisterOffset.Zero, value)` call, which extracts the field values of the record into this register buffer in a single pass. Now the field values are stored in `registers`.
529
+
530
+ The next question is how we can access the field values from the registers? The `Reflect.Record` we built earlier also computes the register layout for each field, which allows us to retrieve each field value from the appropriate register slot using `recordReflect.registers(i).get(registers, RegisterOffset.Zero)`. This call accesses the `i`-th field's value from the registers based on the register layout computed by `Reflect.Record`.
531
+
532
+ Finally, we can iterate through each field, retrieve its value from the registers, force the corresponding `Lazy[Show]` instance for that field's type, and format the result as `fieldName = fieldValue`. The output assembles into the familiar `TypeName(field1 = value1, field2 = value2)` representation.
533
+
534
+ ### Variant Derivation
535
+
536
+ When the derivation process encounters a variant type (e.g., a sealed trait with case classes), it calls the `deriveVariant` method of the `Deriver`. This method receives an `IndexedSeq[Term[F, A, _]]` representing the cases of the variant, along with other metadata such as the type ID, binding information, documentation, modifiers, default values, and examples:
537
+
538
+ ```scala
539
+ def deriveVariant[F[_, _], A](
540
+ cases: IndexedSeq[Term[F, A, ?]],
541
+ typeId: TypeId[A],
542
+ binding: Binding[BindingType.Variant, A],
543
+ doc: Doc,
544
+ modifiers: Seq[Modifier.Reflect],
545
+ defaultValue: Option[A],
546
+ examples: Seq[A]
547
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[A]] = Lazy {
548
+ // Get Show instances for all cases LAZILY
549
+ val caseShowInstances: IndexedSeq[Lazy[Show[Any]]] = cases.map { case_ =>
550
+ D.instance(case_.value.metadata).asInstanceOf[Lazy[Show[Any]]]
551
+ }
552
+ // Cast binding to Binding.Variant to access discriminator and matchers
553
+ val variantBinding = binding.asInstanceOf[Binding.Variant[A]]
554
+ val discriminator = variantBinding.discriminator
555
+ val matchers = variantBinding.matchers
556
+ new Show[A] {
557
+ // Implement show by using discriminator and matchers to find the right case
558
+ // The `value` parameter is of type A (the variant type), e.g. an Option[Int] value
559
+ def show(value: A): String = {
560
+ // Use discriminator to determine which case this value belongs to
561
+ val caseIndex = discriminator.discriminate(value)
562
+ // Use matcher to downcast to the specific case type
563
+ val caseValue = matchers(caseIndex).downcastOrNull(value)
564
+ // Just delegate to the case's Show instance - it already knows its own name
565
+ caseShowInstances(caseIndex).force.show(caseValue)
566
+ }
567
+ }
568
+ }
569
+ ```
570
+
571
+ The derivation process for variants is similar to records, but instead of fields, we have cases. We extract the type class instances for each case, and at runtime we use the discriminator to determine which case the value belongs to. Then we use the matcher to downcast the value to the specific case type.
572
+
573
+ Finally, we extract the corresponding type class instance for that case by applying the case index to the indexed sequence of type class instances. Now we have the correct type class instance for the specific case, wrapped in a `Lazy` data type. We force the lazy wrapper to retrieve the actual type class instance, and then we call the `show` method on that case value to get the string representation.
574
+
575
+ ### Sequence Derivation
576
+
577
+ When the derivation process encounters a sequence type (e.g., `List[A]`), it calls the `deriveSequence` method of the `Deriver`. This method receives a `Reflect[F, A]` representing the element type of the sequence, along with other metadata such as the type ID, binding information, documentation, modifiers, default values, and examples:
578
+
579
+ ```scala
580
+ def deriveSequence[F[_, _], C[_], A](
581
+ element: Reflect[F, A],
582
+ typeId: TypeId[C[A]],
583
+ binding: Binding[BindingType.Seq[C], C[A]],
584
+ doc: Doc,
585
+ modifiers: Seq[Modifier.Reflect],
586
+ defaultValue: Option[C[A]],
587
+ examples: Seq[C[A]]
588
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[C[A]]] = Lazy {
589
+ // Get Show instance for element type (lazily)
590
+ val elementShowLazy: Lazy[Show[A]] = D.instance(element.metadata)
591
+ // Cast binding to Binding.Seq to access the deconstructor
592
+ val seqBinding = binding.asInstanceOf[Binding.Seq[C, A]]
593
+ val deconstructor = seqBinding.deconstructor
594
+ new Show[C[A]] {
595
+ def show(value: C[A]): String = {
596
+ // Use the deconstructor to iterate over elements
597
+ val iterator = deconstructor.deconstruct(value)
598
+ // Force the element Show instance only when actually showing
599
+ val elements = iterator.map(elem => elementShowLazy.force.show(elem)).mkString(", ")
600
+ s"[$elements]"
601
+ }
602
+ }
603
+ }
604
+ ```
605
+
606
+ The derivation process for sequences is straightforward. We extract the type class instance for the element type, and at runtime we use the deconstructor to iterate over the elements of the sequence. For each element, we force the `Lazy[Show[A]]` instance to get the actual `Show[A]` instance, and then call `show` on each element to get its string representation. Finally, we combine all element representations into a single string that represents the entire sequence.
607
+
608
+ ### Map Derivation
609
+
610
+ When the derivation process encounters a map type (e.g., `Map[K, V]`), it calls the `deriveMap` method of the `Deriver`. This method receives `Reflect[F, K]` and `Reflect[F, V]` representing the key and value types of the map, along with other metadata such as the type ID, binding information, documentation, modifiers, default values, and examples:
611
+
612
+ ```scala
613
+ def deriveMap[F[_, _], M[_, _], K, V](
614
+ key: Reflect[F, K],
615
+ value: Reflect[F, V],
616
+ typeId: TypeId[M[K, V]],
617
+ binding: Binding[BindingType.Map[M], M[K, V]],
618
+ doc: Doc,
619
+ modifiers: Seq[Modifier.Reflect],
620
+ defaultValue: Option[M[K, V]],
621
+ examples: Seq[M[K, V]]
622
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[M[K, V]]] = Lazy {
623
+ // Get Show instances for key and value types LAZILY
624
+ val keyShowLazy: Lazy[Show[K]] = D.instance(key.metadata)
625
+ val valueShowLazy: Lazy[Show[V]] = D.instance(value.metadata)
626
+
627
+ // Cast binding to Binding.Map to access the deconstructor
628
+ val mapBinding = binding.asInstanceOf[Binding.Map[M, K, V]]
629
+ val deconstructor = mapBinding.deconstructor
630
+
631
+ new Show[M[K, V]] {
632
+ def show(m: M[K, V]): String = {
633
+ // Use deconstructor to iterate over key-value pairs
634
+ val iterator = deconstructor.deconstruct(m)
635
+ // Force the Show instances only when actually showing
636
+ val entries = iterator.map { kv =>
637
+ val k = deconstructor.getKey(kv)
638
+ val v = deconstructor.getValue(kv)
639
+ s"${keyShowLazy.force.show(k)} -> ${valueShowLazy.force.show(v)}"
640
+ }.mkString(", ")
641
+ s"Map($entries)"
642
+ }
643
+ }
644
+ }
645
+ ```
646
+
647
+ The derivation process for maps is similar to sequences, but we have to handle both keys and values. We extract the type class instances for the key and value types, and at runtime we use the deconstructor to iterate over the key-value pairs of the map. For each pair, we force the `Lazy[Show[K]]` and `Lazy[Show[V]]` instances to get the actual `Show[K]` and `Show[V]` instances, and then call `show` on both the key and value to get their string representations. Finally, we combine all entries into a single string that represents the entire map.
648
+
649
+ ### Dynamic Derivation
650
+
651
+ When the derivation process encounters a dynamic type (e.g., `DynamicValue`), it calls the `deriveDynamic` method of the `Deriver`. This method receives a `Binding[BindingType.Dynamic, DynamicValue]` representing the dynamic type, along with other metadata such as documentation, modifiers, default values, and examples:
652
+
653
+ ```scala
654
+ def deriveDynamic[F[_, _]](
655
+ binding: Binding[BindingType.Dynamic, DynamicValue],
656
+ doc: Doc,
657
+ modifiers: Seq[Modifier.Reflect],
658
+ defaultValue: Option[DynamicValue],
659
+ examples: Seq[DynamicValue]
660
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[DynamicValue]] = Lazy {
661
+ new Show[DynamicValue] {
662
+ def show(value: DynamicValue): String =
663
+ value match {
664
+ case DynamicValue.Primitive(pv) =>
665
+ value.toString
666
+ case DynamicValue.Record(fields) =>
667
+ val fieldStrings = fields.map { case (name, v) =>
668
+ s"$name = ${show(v)}"
669
+ }
670
+ s"Record(${fieldStrings.mkString(", ")})"
671
+ case DynamicValue.Variant(caseName, v) =>
672
+ s"$caseName(${show(v)})"
673
+ case DynamicValue.Sequence(elements) =>
674
+ val elemStrings = elements.map(show)
675
+ s"[${elemStrings.mkString(", ")}]"
676
+ case DynamicValue.Map(entries) =>
677
+ val entryStrings = entries.map { case (k, v) =>
678
+ s"${show(k)} -> ${show(v)}"
679
+ }
680
+ s"Map(${entryStrings.mkString(", ")})"
681
+ case Null =>
682
+ "null"
683
+ }
684
+ }
685
+ }
686
+ ```
687
+
688
+ The derivation process for dynamic types is more complex because the data structure is not known at compile time. Instead, we must handle different cases based on the runtime type of `DynamicValue` using pattern matching. For each subtype: `Primitive` values are converted via `toString`, `Record` fields are recursively shown, `Variant` cases display the name and contained value, `Sequence` elements are shown in bracket notation, `Map` entries are displayed as key-value pairs, and `Null` returns the string "null".
689
+
690
+ ### Wrapper Derivation
691
+
692
+ When the derivation process encounters a wrapper type (e.g., a value class, opaque type, or any type that wraps another type), it calls the `deriveWrapper` method of the `Deriver`. This method receives a `Reflect[F, B]` representing the wrapped (underlying) type, along with other metadata such as the type ID, binding information, documentation, modifiers, default values, and examples:
693
+
694
+ ```scala
695
+ def deriveWrapper[F[_, _], A, B](
696
+ wrapped: Reflect[F, B],
697
+ typeId: TypeId[A],
698
+ binding: Binding[BindingType.Wrapper[A, B], A],
699
+ doc: Doc,
700
+ modifiers: Seq[Modifier.Reflect],
701
+ defaultValue: Option[A],
702
+ examples: Seq[A]
703
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[A]] = Lazy {
704
+ // Get Show instance for the wrapped (underlying) type B LAZILY
705
+ val wrappedShowLazy: Lazy[Show[B]] = D.instance(wrapped.metadata)
706
+
707
+ // Cast binding to Binding.Wrapper to access unwrap function
708
+ val wrapperBinding = binding.asInstanceOf[Binding.Wrapper[A, B]]
709
+
710
+ new Show[A] {
711
+ def show(value: A): String = {
712
+ val unwrapped = wrapperBinding.unwrap(value)
713
+ s"${typeId.name}(${wrappedShowLazy.force.show(unwrapped)})"
714
+ }
715
+ }
716
+ }
717
+ ```
718
+
719
+ The derivation process for wrapper types involves unwrapping the value to access the underlying type. We extract the type class instance for the wrapped type, and at runtime we use the `unwrap` function from the binding to get the underlying value, then show it using its type class instance.
720
+
721
+ ### Example Usages
722
+
723
+ To see how this derivation works in practice, we can define some simple data types and then derive `Show` instances for them using the `DeriveShow` object we implemented.
724
+
725
+ 1. Example 1: Simple `Person` Record with Two Primitive Fields:
726
+
727
+ ```scala
728
+ case class Person(name: String, age: Int)
729
+
730
+ object Person {
731
+ implicit val schema: Schema[Person] = Schema.derived[Person]
732
+ implicit val show: Show[Person] = schema.derive(DeriveShow)
733
+ }
734
+ ```
735
+
736
+ Now we can use the derived `Show[Person]` instance to convert `Person` values to strings:
737
+
738
+ ```scala
739
+ Person.show.show(Person("Alice", 30))
740
+ // res7: String = "Person(name = \"Alice\", age = 30)"
741
+ ```
742
+
743
+ 2. Simple Shape Variant (Circle, Rectangle)
744
+
745
+ ```scala
746
+ sealed trait Shape
747
+ case class Circle(radius: Double) extends Shape
748
+ case class Rectangle(width: Double, height: Double) extends Shape
749
+
750
+ object Shape {
751
+ implicit val schema: Schema[Shape] = Schema.derived[Shape]
752
+ implicit val show: Show[Shape] = schema.derive(DeriveShow)
753
+ }
754
+ ```
755
+
756
+ To show a `Shape` value, we can do the following:
757
+
758
+ ```scala
759
+ val shape1: Shape = Circle(5.0)
760
+ // shape1: Shape = Circle(5.0)
761
+ Shape.show.show(shape1)
762
+ // res8: String = "Circle(radius = 5.0)"
763
+
764
+ val shape2: Shape = Rectangle(4.0, 6.0)
765
+ // shape2: Shape = Rectangle(width = 4.0, height = 6.0)
766
+ Shape.show.show(shape2)
767
+ // res9: String = "Rectangle(width = 4.0, height = 6.0)"
768
+ ```
769
+
770
+ 3. Recursive Tree and Expr
771
+
772
+ ```scala
773
+ case class Tree(value: Int, children: List[Tree])
774
+ object Tree {
775
+ implicit val schema: Schema[Tree] = Schema.derived[Tree]
776
+ implicit val show: Show[Tree] = schema.derive(DeriveShow)
777
+ }
778
+ ```
779
+
780
+ The `Tree` is a record with a recursive field `children` of type `List[Tree]`. Let's see how the derived `Show[Tree]` instance handles this recursive structure:
781
+
782
+ ```scala
783
+ val tree = Tree(1, List(Tree(2, List(Tree(4, Nil))), Tree(3, Nil)))
784
+ // tree: Tree = Tree(
785
+ // value = 1,
786
+ // children = List(
787
+ // Tree(value = 2, children = List(Tree(value = 4, children = List()))),
788
+ // Tree(value = 3, children = List())
789
+ // )
790
+ // )
791
+ Tree.show.show(tree)
792
+ // res10: String = "Tree(value = 1, children = [Tree(value = 2, children = [Tree(value = 4, children = [])]), Tree(value = 3, children = [])])"
793
+ ```
794
+
795
+ 4. Example 4: Recursive Sealed Trait (Expr)
796
+
797
+ ```scala
798
+ sealed trait Expr
799
+ case class Num(n: Int) extends Expr
800
+ case class Add(a: Expr, b: Expr) extends Expr
801
+
802
+ object Expr {
803
+ implicit val schema: Schema[Expr] = Schema.derived[Expr]
804
+ implicit val show: Show[Expr] = schema.derive(DeriveShow)
805
+ }
806
+ ```
807
+
808
+ Similar to `Tree`, `Expr` is a recursive variant type. The derived `Show[Expr]` instance can handle this recursive structure as well:
809
+
810
+ ```scala
811
+ val expr: Expr = Add(Num(1), Add(Num(2), Num(3)))
812
+ // expr: Expr = Add(a = Num(1), b = Add(a = Num(2), b = Num(3)))
813
+ Expr.show.show(expr)
814
+ // res11: String = "Add(a = Num(n = 1), b = Add(a = Num(n = 2), b = Num(n = 3)))"
815
+ ```
816
+
817
+ 5. Example 5: DynamicValue Example
818
+
819
+ ```scala
820
+ implicit val dynamicShow: Show[DynamicValue] = Schema.dynamic.derive(DeriveShow)
821
+ ```
822
+
823
+ Let's define a `DynamicValue` that represents a record with some primitive fields and a sequence field, then show it using the derived `Show[DynamicValue]` instance:
824
+
825
+ ```scala
826
+ val manualRecord = DynamicValue.Record(
827
+ Chunk(
828
+ "id" -> DynamicValue.Primitive(PrimitiveValue.Int(42)),
829
+ "title" -> DynamicValue.Primitive(PrimitiveValue.String("Hello World")),
830
+ "tags" -> DynamicValue.Sequence(
831
+ Chunk(
832
+ DynamicValue.Primitive(PrimitiveValue.String("scala")),
833
+ DynamicValue.Primitive(PrimitiveValue.String("zio"))
834
+ )
835
+ )
836
+ )
837
+ )
838
+ // manualRecord: Record = Record(
839
+ // IndexedSeq(
840
+ // ("id", Primitive(Int(42))),
841
+ // ("title", Primitive(String("Hello World"))),
842
+ // (
843
+ // "tags",
844
+ // Sequence(IndexedSeq(Primitive(String("scala")), Primitive(String("zio"))))
845
+ // )
846
+ // )
847
+ // )
848
+
849
+ dynamicShow.show(manualRecord)
850
+ // res12: String = "Record(id = 42, title = \"Hello World\", tags = [\"scala\", \"zio\"])"
851
+ ```
852
+
853
+ 6. Example 6: Simple Email Wrapper Type
854
+
855
+ ```scala
856
+ case class Email(value: String)
857
+ object Email {
858
+ implicit val schema: Schema[Email] = Schema[String].transform(
859
+ Email(_),
860
+ _.value
861
+ )
862
+ implicit val show: Show[Email] = schema.derive(DeriveShow)
863
+ }
864
+ ```
865
+
866
+ The `Email` type is a simple wrapper around `String`. Let's see how it shows an `Email` value:
867
+
868
+ ```scala
869
+ val email = Email("alice@example.com")
870
+ // email: Email = Email("alice@example.com")
871
+ println(s"Email: ${Email.show.show(email)}")
872
+ // Email: Email("alice@example.com")
873
+ ```
874
+
875
+ ## Example 2: Deriving a `Gen` Type Class Instance
876
+
877
+ Let's say we want to derive a `Gen` type class instance for any type `A`:
878
+
879
+ ```scala
880
+ import scala.util.Random
881
+
882
+ trait Gen[A] {
883
+ def generate(random: Random): A
884
+ }
885
+ ```
886
+
887
+ Unlike `Show`, which is a type class for converting values of type `A` to something else (a `String`)—so you can think of it as a function of type `A => Output (String)`—the `Gen` type class is for generating values of type `A`. You can think of it as a function of type `Input (Random) => A`.
888
+
889
+ To implement the `Show` type class, we need to know what components type `A` is made up of, so we can convert each component to a `String` and combine them to form the final `String` representation of `A`. To do this, we need to be able to deconstruct a value of type `A` into its components. On the other hand, to implement the `Gen` type class, we need to know how to generate each component of type `A` using a `Random` input, and then combine those generated components to form a complete value of type `A`. This means that for `Gen`, we need to be able to construct a value of type `A` from its components, rather than deconstructing it. Therefore, in the derivation methods for `Gen`, we will use the `constructor` from the `Binding` to create values of type `A` from generated components.
890
+
891
+ Here is a simple pedagogical implementation of a `GenDeriver` that can derive `Gen` instances for various types:
892
+
893
+ ```scala
894
+ import zio.blocks.chunk.Chunk
895
+ import zio.blocks.schema.*
896
+ import zio.blocks.schema.binding.*
897
+ import zio.blocks.schema.derive.Deriver
898
+ import zio.blocks.typeid.TypeId
899
+
900
+ object DeriveGen extends Deriver[Gen] {
901
+
902
+ override def derivePrimitive[A](
903
+ primitiveType: PrimitiveType[A],
904
+ typeId: TypeId[A],
905
+ binding: Binding[BindingType.Primitive, A],
906
+ doc: Doc,
907
+ modifiers: Seq[Modifier.Reflect],
908
+ defaultValue: Option[A],
909
+ examples: Seq[A]
910
+ ): Lazy[Gen[A]] =
911
+ Lazy {
912
+ new Gen[A] {
913
+ def generate(random: Random): A = primitiveType match {
914
+ case _: PrimitiveType.String => random.alphanumeric.take(random.nextInt(10) + 1).mkString.asInstanceOf[A]
915
+ case _: PrimitiveType.Char => random.alphanumeric.head.asInstanceOf[A]
916
+ case _: PrimitiveType.Boolean => random.nextBoolean().asInstanceOf[A]
917
+ case _: PrimitiveType.Int => random.nextInt().asInstanceOf[A]
918
+ case _: PrimitiveType.Long => random.nextLong().asInstanceOf[A]
919
+ case _: PrimitiveType.Double => random.nextDouble().asInstanceOf[A]
920
+ case PrimitiveType.Unit => ().asInstanceOf[A]
921
+ // For brevity, other primitives default to their zero/empty value
922
+ // In a real implementation, you'd want to handle all primitives and possibly use modifiers for ranges, etc.
923
+ case _ =>
924
+ defaultValue.getOrElse {
925
+ throw new IllegalArgumentException(
926
+ s"Gen derivation not implemented for primitive type $primitiveType " +
927
+ s"(typeId = $typeId) and no default value provided."
928
+ )
929
+ }
930
+ }
931
+ }
932
+ }
933
+
934
+ /**
935
+ * Strategy:
936
+ * 1. Get Gen type class instances for each field
937
+ * 2. Generate random values for each field
938
+ * 3. Use the constructor to build the record
939
+ */
940
+ override def deriveRecord[F[_, _], A](
941
+ fields: IndexedSeq[Term[F, A, ?]],
942
+ typeId: TypeId[A],
943
+ binding: Binding[BindingType.Record, A],
944
+ doc: Doc,
945
+ modifiers: Seq[Modifier.Reflect],
946
+ defaultValue: Option[A],
947
+ examples: Seq[A]
948
+ )(implicit F: HasBinding[F], D: DeriveGen.HasInstance[F]): Lazy[Gen[A]] =
949
+ Lazy {
950
+ // Get Gen instances for each field
951
+ val fieldGens: IndexedSeq[Lazy[Gen[Any]]] = fields.map { field =>
952
+ D.instance(field.value.metadata).asInstanceOf[Lazy[Gen[Any]]]
953
+ }
954
+
955
+ // Build Reflect.Record to access registers and constructor
956
+ val recordFields = fields.asInstanceOf[IndexedSeq[Term[Binding, A, ?]]]
957
+ val recordBinding = binding.asInstanceOf[Binding.Record[A]]
958
+ val recordReflect = new Reflect.Record[Binding, A](recordFields, typeId, recordBinding, doc, modifiers)
959
+
960
+ new Gen[A] {
961
+ def generate(random: Random): A = {
962
+ // Create registers to hold field values
963
+ val registers = Registers(recordReflect.usedRegisters)
964
+
965
+ // Generate each field and store in registers
966
+ fields.indices.foreach { i =>
967
+ val value = fieldGens(i).force.generate(random)
968
+ recordReflect.registers(i).set(registers, RegisterOffset.Zero, value)
969
+ }
970
+
971
+ // Construct the record from registers
972
+ recordBinding.constructor.construct(registers, RegisterOffset.Zero)
973
+ }
974
+ }
975
+ }
976
+
977
+ /**
978
+ * Strategy:
979
+ * 1. Get Gen type class instances for all cases
980
+ * 2. Randomly pick a case
981
+ * 3. Generate a value for that case
982
+ */
983
+ override def deriveVariant[F[_, _], A](
984
+ cases: IndexedSeq[Term[F, A, ?]],
985
+ typeId: TypeId[A],
986
+ binding: Binding[BindingType.Variant, A],
987
+ doc: Doc,
988
+ modifiers: Seq[Modifier.Reflect],
989
+ defaultValue: Option[A],
990
+ examples: Seq[A]
991
+ )(implicit F: HasBinding[F], D: DeriveGen.HasInstance[F]): Lazy[Gen[A]] = Lazy {
992
+ // Get Gen instances for all cases
993
+ val caseGens: IndexedSeq[Lazy[Gen[A]]] = cases.map { c =>
994
+ D.instance(c.value.metadata).asInstanceOf[Lazy[Gen[A]]]
995
+ }
996
+
997
+ new Gen[A] {
998
+ def generate(random: Random): A = {
999
+ // Pick a random case and generate its value
1000
+ val caseIndex = random.nextInt(cases.length)
1001
+ caseGens(caseIndex).force.generate(random)
1002
+ }
1003
+ }
1004
+ }
1005
+
1006
+ /**
1007
+ * Strategy:
1008
+ * 1. Get Gen type class instances for the element type
1009
+ * 2. Generate 0-5 elements
1010
+ * 3. Build the collection using the constructor
1011
+ */
1012
+ override def deriveSequence[F[_, _], C[_], A](
1013
+ element: Reflect[F, A],
1014
+ typeId: TypeId[C[A]],
1015
+ binding: Binding[BindingType.Seq[C], C[A]],
1016
+ doc: Doc,
1017
+ modifiers: Seq[Modifier.Reflect],
1018
+ defaultValue: Option[C[A]],
1019
+ examples: Seq[C[A]]
1020
+ )(implicit F: HasBinding[F], D: DeriveGen.HasInstance[F]): Lazy[Gen[C[A]]] = Lazy {
1021
+ val elementGen = D.instance(element.metadata)
1022
+ val seqBinding = binding.asInstanceOf[Binding.Seq[C, A]]
1023
+ val constructor = seqBinding.constructor
1024
+
1025
+ new Gen[C[A]] {
1026
+ def generate(random: Random): C[A] = {
1027
+ val length = random.nextInt(6) // 0 to 5 elements
1028
+ implicit val ct: scala.reflect.ClassTag[A] = scala.reflect.ClassTag.Any.asInstanceOf[scala.reflect.ClassTag[A]]
1029
+
1030
+ if (length == 0) {
1031
+ constructor.empty[A]
1032
+ } else {
1033
+ val builder = constructor.newBuilder[A](length)
1034
+ (0 until length).foreach { _ =>
1035
+ constructor.add(builder, elementGen.force.generate(random))
1036
+ }
1037
+ constructor.result(builder)
1038
+ }
1039
+ }
1040
+ }
1041
+ }
1042
+
1043
+ /**
1044
+ * Strategy:
1045
+ * 1. Get Gen type class instances for key and value types
1046
+ * 2. Generate 0-5 key-value pairs
1047
+ * 3. Build the map using the constructor
1048
+ */
1049
+ override def deriveMap[F[_, _], M[_, _], K, V](
1050
+ key: Reflect[F, K],
1051
+ value: Reflect[F, V],
1052
+ typeId: TypeId[M[K, V]],
1053
+ binding: Binding[BindingType.Map[M], M[K, V]],
1054
+ doc: Doc,
1055
+ modifiers: Seq[Modifier.Reflect],
1056
+ defaultValue: Option[M[K, V]],
1057
+ examples: Seq[M[K, V]]
1058
+ )(implicit F: HasBinding[F], D: DeriveGen.HasInstance[F]): Lazy[Gen[M[K, V]]] = Lazy {
1059
+ val keyGen = D.instance(key.metadata)
1060
+ val valueGen = D.instance(value.metadata)
1061
+ val mapBinding = binding.asInstanceOf[Binding.Map[M, K, V]]
1062
+ val constructor = mapBinding.constructor
1063
+
1064
+ new Gen[M[K, V]] {
1065
+ def generate(random: Random): M[K, V] = {
1066
+ val size = random.nextInt(6) // 0 to 5 entries
1067
+
1068
+ if (size == 0) {
1069
+ constructor.emptyObject[K, V]
1070
+ } else {
1071
+ val builder = constructor.newObjectBuilder[K, V](size)
1072
+ (0 until size).foreach { _ =>
1073
+ constructor.addObject(builder, keyGen.force.generate(random), valueGen.force.generate(random))
1074
+ }
1075
+ constructor.resultObject(builder)
1076
+ }
1077
+ }
1078
+ }
1079
+ }
1080
+
1081
+ /**
1082
+ * Since DynamicValue can represent any schema type, we generate random
1083
+ * dynamic values by randomly choosing a variant and generating appropriate
1084
+ * content.
1085
+ */
1086
+ override def deriveDynamic[F[_, _]](
1087
+ binding: Binding[BindingType.Dynamic, DynamicValue],
1088
+ doc: Doc,
1089
+ modifiers: Seq[Modifier.Reflect],
1090
+ defaultValue: Option[DynamicValue],
1091
+ examples: Seq[DynamicValue]
1092
+ )(implicit F: HasBinding[F], D: DeriveGen.HasInstance[F]): Lazy[Gen[DynamicValue]] = Lazy {
1093
+ new Gen[DynamicValue] {
1094
+ // Helper to generate a random primitive value
1095
+ private def randomPrimitive(random: Random): DynamicValue.Primitive = {
1096
+ val primitiveType = random.nextInt(5)
1097
+ primitiveType match {
1098
+ case 0 => DynamicValue.Primitive(PrimitiveValue.Int(random.nextInt()))
1099
+ case 1 => DynamicValue.Primitive(PrimitiveValue.String(random.alphanumeric.take(10).mkString))
1100
+ case 2 => DynamicValue.Primitive(PrimitiveValue.Boolean(random.nextBoolean()))
1101
+ case 3 => DynamicValue.Primitive(PrimitiveValue.Double(random.nextDouble()))
1102
+ case 4 => DynamicValue.Primitive(PrimitiveValue.Long(random.nextLong()))
1103
+ }
1104
+ }
1105
+
1106
+ def generate(random: Random): DynamicValue = {
1107
+ // Randomly choose what kind of DynamicValue to generate
1108
+ // Weight towards primitives and simpler structures to avoid deep nesting
1109
+ val valueType = random.nextInt(10)
1110
+ valueType match {
1111
+ case 0 | 1 | 2 | 3 | 4 =>
1112
+ // 50% chance: generate a primitive
1113
+ randomPrimitive(random)
1114
+
1115
+ case 5 | 6 =>
1116
+ // 20% chance: generate a record with 1-3 fields
1117
+ val numFields = random.nextInt(3) + 1
1118
+ val fields = (0 until numFields).map { i =>
1119
+ val fieldName = s"field$i"
1120
+ val fieldValue = randomPrimitive(random)
1121
+ (fieldName, fieldValue: DynamicValue)
1122
+ }
1123
+ DynamicValue.Record(Chunk.from(fields))
1124
+
1125
+ case 7 | 8 =>
1126
+ // 20% chance: generate a sequence of 0-3 primitives
1127
+ val numElements = random.nextInt(4)
1128
+ val elements = (0 until numElements).map(_ => randomPrimitive(random): DynamicValue)
1129
+ DynamicValue.Sequence(Chunk.from(elements))
1130
+
1131
+ case 9 =>
1132
+ // 10% chance: generate null
1133
+ DynamicValue.Null
1134
+ }
1135
+ }
1136
+ }
1137
+ }
1138
+
1139
+ override def deriveWrapper[F[_, _], A, B](
1140
+ wrapped: Reflect[F, B],
1141
+ typeId: TypeId[A],
1142
+ binding: Binding[BindingType.Wrapper[A, B], A],
1143
+ doc: Doc,
1144
+ modifiers: Seq[Modifier.Reflect],
1145
+ defaultValue: Option[A],
1146
+ examples: Seq[A]
1147
+ )(implicit F: HasBinding[F], D: DeriveGen.HasInstance[F]): Lazy[Gen[A]] = Lazy {
1148
+ val wrappedGen = D.instance(wrapped.metadata)
1149
+ val wrapperBinding = binding.asInstanceOf[Binding.Wrapper[A, B]]
1150
+
1151
+ new Gen[A] {
1152
+ def generate(random: Random): A =
1153
+ wrapperBinding.wrap(wrappedGen.force.generate(random))
1154
+ }
1155
+ }
1156
+ }
1157
+ ```
1158
+
1159
+ ### Primitive Derivation
1160
+
1161
+ The `derivePrimitive` method is responsible for deriving a `Gen` instance for primitive types. It matches on the specific primitive type and generates random values accordingly. For example, for `String`, it generates a random alphanumeric string of random length; for `Int`, it generates a random integer; and so on. The generated value is then cast to the appropriate type `A` and returned:
1162
+
1163
+ ```scala
1164
+ def derivePrimitive[A](
1165
+ primitiveType: PrimitiveType[A],
1166
+ typeId: TypeId[A],
1167
+ binding: Binding[BindingType.Primitive, A],
1168
+ doc: Doc,
1169
+ modifiers: Seq[Modifier.Reflect],
1170
+ defaultValue: Option[A],
1171
+ examples: Seq[A]
1172
+ ): Lazy[Gen[A]] =
1173
+ Lazy {
1174
+ new Gen[A] {
1175
+ def generate(random: Random): A = primitiveType match {
1176
+ case _: PrimitiveType.String => random.alphanumeric.take(random.nextInt(10) + 1).mkString.asInstanceOf[A]
1177
+ case _: PrimitiveType.Char => random.alphanumeric.head.asInstanceOf[A]
1178
+ case _: PrimitiveType.Boolean => random.nextBoolean().asInstanceOf[A]
1179
+ case _: PrimitiveType.Int => random.nextInt(100).asInstanceOf[A]
1180
+ case _: PrimitiveType.Long => random.nextLong().asInstanceOf[A]
1181
+ case _: PrimitiveType.Double => random.nextDouble().asInstanceOf[A]
1182
+ case PrimitiveType.Unit => ().asInstanceOf[A]
1183
+ // For brevity, other primitives default to their zero/empty value
1184
+ // In a real implementation, you would want to handle all primitives and possibly use modifiers for ranges, etc.
1185
+ case _ => defaultValue.getOrElse(null.asInstanceOf[A])
1186
+ }
1187
+ }
1188
+ }
1189
+ ```
1190
+
1191
+ To handle all primitive types, you would want to implement cases for each primitive type defined in your schema system. In a real implementation, you might also want to consider using modifiers to allow users to specify constraints on the generated values (e.g., string length, numeric ranges, etc.).
1192
+
1193
+ ### Record Derivation
1194
+
1195
+ The `deriveRecord` method is responsible for deriving a `Gen` instance for record types, such as case classes and tuples. The strategy for deriving a record type involves three main steps:
1196
+
1197
+ ```scala
1198
+ def deriveRecord[F[_, _], A](
1199
+ fields: IndexedSeq[Term[F, A, ?]],
1200
+ typeId: TypeId[A],
1201
+ binding: Binding[BindingType.Record, A],
1202
+ doc: Doc,
1203
+ modifiers: Seq[Modifier.Reflect],
1204
+ defaultValue: Option[A],
1205
+ examples: Seq[A]
1206
+ )(implicit F: HasBinding[F], D: DeriveGen.HasInstance[F]): Lazy[Gen[A]] =
1207
+ Lazy {
1208
+ // Get Gen instances for each field
1209
+ val fieldGens: IndexedSeq[Lazy[Gen[Any]]] = fields.map { field =>
1210
+ D.instance(field.value.metadata).asInstanceOf[Lazy[Gen[Any]]]
1211
+ }
1212
+
1213
+ // Build Reflect.Record to access registers and constructor
1214
+ val recordFields = fields.asInstanceOf[IndexedSeq[Term[Binding, A, ?]]]
1215
+ val recordBinding = binding.asInstanceOf[Binding.Record[A]]
1216
+ val recordReflect = new Reflect.Record[Binding, A](recordFields, typeId, recordBinding, doc, modifiers)
1217
+
1218
+ new Gen[A] {
1219
+ def generate(random: Random): A = {
1220
+ // Create registers to hold field values
1221
+ val registers = Registers(recordReflect.usedRegisters)
1222
+
1223
+ // Generate each field and store in registers
1224
+ fields.indices.foreach { i =>
1225
+ val value = fieldGens(i).force.generate(random)
1226
+ recordReflect.registers(i).set(registers, RegisterOffset.Zero, value)
1227
+ }
1228
+
1229
+ // Construct the record from registers
1230
+ recordBinding.constructor.construct(registers, RegisterOffset.Zero)
1231
+ }
1232
+ }
1233
+ }
1234
+ ```
1235
+
1236
+ As shown above, the implementation of the `deriveRecord` method for `Gen` is structurally similar to the `deriveRecord` method used in `Show` derivation. The primary difference is the data flow: instead of deconstructing an existing record to access its fields, we generate random values for each field. We then use `Register#set` to store these values in the registers before invoking the `constructor` from the `Binding` to create an instance of type `A`.
1237
+
1238
+ ### Variant Derivation
1239
+
1240
+ The `deriveVariant` method is responsible for deriving a `Gen` instance for variant types, such as sealed traits with case classes:
1241
+
1242
+ ```scala
1243
+ def deriveVariant[F[_, _], A](
1244
+ cases: IndexedSeq[Term[F, A, ?]],
1245
+ typeId: TypeId[A],
1246
+ binding: Binding[BindingType.Variant, A],
1247
+ doc: Doc,
1248
+ modifiers: Seq[Modifier.Reflect],
1249
+ defaultValue: Option[A],
1250
+ examples: Seq[A]
1251
+ )(implicit F: HasBinding[F], D: DeriveGen.HasInstance[F]): Lazy[Gen[A]] = Lazy {
1252
+ // Get Gen instances for all cases
1253
+ val caseGens: IndexedSeq[Lazy[Gen[A]]] = cases.map { c =>
1254
+ D.instance(c.value.metadata).asInstanceOf[Lazy[Gen[A]]]
1255
+ }
1256
+
1257
+ new Gen[A] {
1258
+ def generate(random: Random): A = {
1259
+ // Pick a random case and generate its value
1260
+ val caseIndex = random.nextInt(cases.length)
1261
+ caseGens(caseIndex).force.generate(random)
1262
+ }
1263
+ }
1264
+ }
1265
+ ```
1266
+
1267
+ The derivation process for `Gen` variants is simpler than for the record case because we don't need to worry about registers or constructors. Instead, we simply need to randomly select one of the type class instances for the cases and generate a value for that case.
1268
+
1269
+ ### Sequence Derivation
1270
+
1271
+ The `deriveSequence` method is responsible for deriving a `Gen` instance for sequence types, such as `List[A]`:
1272
+
1273
+ ```scala
1274
+ def deriveSequence[F[_, _], C[_], A](
1275
+ element: Reflect[F, A],
1276
+ typeId: TypeId[C[A]],
1277
+ binding: Binding[BindingType.Seq[C], C[A]],
1278
+ doc: Doc,
1279
+ modifiers: Seq[Modifier.Reflect],
1280
+ defaultValue: Option[C[A]],
1281
+ examples: Seq[C[A]]
1282
+ )(implicit F: HasBinding[F], D: DeriveGen.HasInstance[F]): Lazy[Gen[C[A]]] = Lazy {
1283
+ val elementGen = D.instance(element.metadata)
1284
+ val seqBinding = binding.asInstanceOf[Binding.Seq[C, A]]
1285
+ val constructor = seqBinding.constructor
1286
+
1287
+ new Gen[C[A]] {
1288
+ def generate(random: Random): C[A] = {
1289
+ val length = random.nextInt(6) // 0 to 5 elements
1290
+ implicit val ct: scala.reflect.ClassTag[A] = scala.reflect.ClassTag.Any.asInstanceOf[scala.reflect.ClassTag[A]]
1291
+
1292
+ if (length == 0) {
1293
+ constructor.empty[A]
1294
+ } else {
1295
+ val builder = constructor.newBuilder[A](length)
1296
+ (0 until length).foreach { _ =>
1297
+ constructor.add(builder, elementGen.force.generate(random))
1298
+ }
1299
+ constructor.result(builder)
1300
+ }
1301
+ }
1302
+ }
1303
+ }
1304
+ ```
1305
+
1306
+ A sequence is an object that contains multiple elements of the same type. To derive a `Gen` instance for a sequence, we first need to retrieve the `Gen` instance for the element type. Then, at runtime, we generate a random length for the sequence (e.g., between 0 and 5). Based on this length, we either return an empty sequence using `constructor.empty` or create a new builder using `constructor.newBuilder`. We then generate random values for each element using the element's type class instance and add them to the builder using `constructor.add`. Finally, we call `constructor.result` to build the final sequence object.
1307
+
1308
+ ### Map Derivation
1309
+
1310
+ The `deriveMap` method is responsible for deriving a `Gen` instance for map types, such as `Map[K, V]`:
1311
+
1312
+ ```scala
1313
+ def deriveMap[F[_, _], M[_, _], K, V](
1314
+ key: Reflect[F, K],
1315
+ value: Reflect[F, V],
1316
+ typeId: TypeId[M[K, V]],
1317
+ binding: Binding[BindingType.Map[M], M[K, V]],
1318
+ doc: Doc,
1319
+ modifiers: Seq[Modifier.Reflect],
1320
+ defaultValue: Option[M[K, V]],
1321
+ examples: Seq[M[K, V]]
1322
+ )(implicit F: HasBinding[F], D: DeriveGen.HasInstance[F]): Lazy[Gen[M[K, V]]] = Lazy {
1323
+ val keyGen = D.instance(key.metadata)
1324
+ val valueGen = D.instance(value.metadata)
1325
+ val mapBinding = binding.asInstanceOf[Binding.Map[M, K, V]]
1326
+ val constructor = mapBinding.constructor
1327
+
1328
+ new Gen[M[K, V]] {
1329
+ def generate(random: Random): M[K, V] = {
1330
+ val size = random.nextInt(6) // 0 to 5 entries
1331
+
1332
+ if (size == 0) {
1333
+ constructor.emptyObject[K, V]
1334
+ } else {
1335
+ val builder = constructor.newObjectBuilder[K, V](size)
1336
+ (0 until size).foreach { _ =>
1337
+ constructor.addObject(builder, keyGen.force.generate(random), valueGen.force.generate(random))
1338
+ }
1339
+ constructor.resultObject(builder)
1340
+ }
1341
+ }
1342
+ }
1343
+ }
1344
+ ```
1345
+
1346
+ The derivation process for maps is similar to sequences, but it requires handling the generation of random values for both keys and values.
1347
+
1348
+ ### Dynamic Derivation
1349
+
1350
+ The `deriveDynamic` method is responsible for deriving a `Gen` instance for dynamic types, such as `DynamicValue`. Since `DynamicValue` can represent any schema type, we generate random dynamic values by choosing a variant at random and generating the appropriate content for that variant. The implementation involves pattern matching on the `DynamicValue` type and generating content accordingly:
1351
+
1352
+ ```scala
1353
+ def deriveDynamic[F[_, _]](
1354
+ binding: Binding[BindingType.Dynamic, DynamicValue],
1355
+ doc: Doc,
1356
+ modifiers: Seq[Modifier.Reflect],
1357
+ defaultValue: Option[DynamicValue],
1358
+ examples: Seq[DynamicValue]
1359
+ )(implicit F: HasBinding[F], D: DeriveGen.HasInstance[F]): Lazy[Gen[DynamicValue]] = Lazy {
1360
+ new Gen[DynamicValue] {
1361
+ // Helper to generate a random primitive value
1362
+ private def randomPrimitive(random: Random): DynamicValue.Primitive = {
1363
+ val primitiveType = random.nextInt(5)
1364
+ primitiveType match {
1365
+ case 0 => DynamicValue.Primitive(PrimitiveValue.Int(random.nextInt()))
1366
+ case 1 => DynamicValue.Primitive(PrimitiveValue.String(random.alphanumeric.take(10).mkString))
1367
+ case 2 => DynamicValue.Primitive(PrimitiveValue.Boolean(random.nextBoolean()))
1368
+ case 3 => DynamicValue.Primitive(PrimitiveValue.Double(random.nextDouble()))
1369
+ case 4 => DynamicValue.Primitive(PrimitiveValue.Long(random.nextLong()))
1370
+ }
1371
+ }
1372
+
1373
+ def generate(random: Random): DynamicValue = {
1374
+ // Randomly choose what kind of DynamicValue to generate
1375
+ // Weight towards primitives and simpler structures to avoid deep nesting
1376
+ val valueType = random.nextInt(10)
1377
+ valueType match {
1378
+ case 0 | 1 | 2 | 3 | 4 =>
1379
+ // 50% chance: generate a primitive
1380
+ randomPrimitive(random)
1381
+
1382
+ case 5 | 6 =>
1383
+ // 20% chance: generate a record with 1-3 fields
1384
+ val numFields = random.nextInt(3) + 1
1385
+ val fields = (0 until numFields).map { i =>
1386
+ val fieldName = s"field$i"
1387
+ val fieldValue = randomPrimitive(random)
1388
+ (fieldName, fieldValue: DynamicValue)
1389
+ }
1390
+ DynamicValue.Record(Chunk.from(fields))
1391
+
1392
+ case 7 | 8 =>
1393
+ // 20% chance: generate a sequence of 0-3 primitives
1394
+ val numElements = random.nextInt(4)
1395
+ val elements = (0 until numElements).map(_ => randomPrimitive(random): DynamicValue)
1396
+ DynamicValue.Sequence(Chunk.from(elements))
1397
+
1398
+ case 9 =>
1399
+ // 10% chance: generate null
1400
+ DynamicValue.Null
1401
+ }
1402
+ }
1403
+ }
1404
+ }
1405
+ ```
1406
+
1407
+ Please note that the random generation logic in this example is basic and is intended for illustrative purposes only.
1408
+
1409
+ ### Wrapper Derivation
1410
+
1411
+ The `deriveWrapper` method is responsible for deriving a `Gen` instance for wrapper types, such as value classes or opaque types:
1412
+
1413
+ ```scala
1414
+ def deriveWrapper[F[_, _], A, B](
1415
+ wrapped: Reflect[F, B],
1416
+ typeId: TypeId[A],
1417
+ binding: Binding[BindingType.Wrapper[A, B], A],
1418
+ doc: Doc,
1419
+ modifiers: Seq[Modifier.Reflect],
1420
+ defaultValue: Option[A],
1421
+ examples: Seq[A]
1422
+ )(implicit F: HasBinding[F], D: DeriveGen.HasInstance[F]): Lazy[Gen[A]] = Lazy {
1423
+ val wrappedGen = D.instance(wrapped.metadata)
1424
+ val wrapperBinding = binding.asInstanceOf[Binding.Wrapper[A, B]]
1425
+
1426
+ new Gen[A] {
1427
+ def generate(random: Random): A =
1428
+ wrapperBinding.wrap(wrappedGen.force.generate(random))
1429
+ }
1430
+ }
1431
+ ```
1432
+
1433
+ First, we retrieve the `Gen` instance for the wrapped (underlying) type `B`. Then, within the `generate` method, we generate a random value of type `B` and wrap it into type `A` using the `wrap` function provided by the binding.
1434
+
1435
+ ### Example Usages
1436
+
1437
+ To see how this derivation works in practice, we can define some simple data types and then derive `Gen` instances for them using the `DeriveGen` object we implemented.
1438
+
1439
+ 1. Example 1: Simple `Person` Record with Two Primitive Fields:
1440
+
1441
+ ```scala
1442
+ case class Person(name: String, age: Int)
1443
+
1444
+ object Person {
1445
+ implicit val schema: Schema[Person] = Schema.derived[Person]
1446
+ implicit val gen: Gen[Person] = schema.derive(DeriveGen)
1447
+ }
1448
+ ```
1449
+
1450
+ Now we can use the derived `Gen[Person]` instance to generate random `Person` values:
1451
+
1452
+ ```scala
1453
+ val random = new Random(42) // Seeded for reproducible output
1454
+ // random: Random = scala.util.Random@1247c2a2
1455
+
1456
+ Person.gen.generate(random)
1457
+ // res14: Person = Person(name = "p", age = -1360544799)
1458
+ Person.gen.generate(random)
1459
+ // res15: Person = Person(name = "C7DgX", age = 392236186)
1460
+ Person.gen.generate(random)
1461
+ // res16: Person = Person(name = "AM6", age = 1184328952)
1462
+ ```
1463
+
1464
+ 2. Simple Shape Variant (Circle, Rectangle)
1465
+
1466
+ ```scala
1467
+ sealed trait Shape
1468
+ case class Circle(radius: Double) extends Shape
1469
+ case class Rectangle(width: Double, height: Double) extends Shape
1470
+
1471
+ object Shape {
1472
+ implicit val schema: Schema[Shape] = Schema.derived[Shape]
1473
+ implicit val gen: Gen[Shape] = schema.derive(DeriveGen)
1474
+ }
1475
+ ```
1476
+
1477
+ To generate random `Shape` values, we can do the following:
1478
+
1479
+ ```scala
1480
+ Shape.gen.generate(random)
1481
+ // res17: Shape = Rectangle(
1482
+ // width = 0.46365357580915334,
1483
+ // height = 0.7829017787900358
1484
+ // )
1485
+ Shape.gen.generate(random)
1486
+ // res18: Shape = Rectangle(
1487
+ // width = 0.15195824856297624,
1488
+ // height = 0.43979982659080874
1489
+ // )
1490
+ Shape.gen.generate(random)
1491
+ // res19: Shape = Rectangle(
1492
+ // width = 0.38656687435934867,
1493
+ // height = 0.17737847790937833
1494
+ // )
1495
+ Shape.gen.generate(random)
1496
+ // res20: Shape = Rectangle(
1497
+ // width = 0.338307935145014,
1498
+ // height = 0.2506613258416336
1499
+ // )
1500
+ ```
1501
+
1502
+ 3. Team with Sequence of Members (List)
1503
+
1504
+ ```scala
1505
+ case class Team(members: List[String])
1506
+
1507
+ object Team {
1508
+ implicit val schema: Schema[Team] = Schema.derived[Team]
1509
+ implicit val gen: Gen[Team] = schema.derive(DeriveGen)
1510
+ }
1511
+ ```
1512
+
1513
+ Let's generate some random `Team` values:
1514
+
1515
+ ```scala
1516
+ Team.gen.generate(random)
1517
+ // res21: Team = Team(List("zZY", "TZlZMZdVjx", "G", "iqf1Pt9", "S1q6qHNj0R"))
1518
+ Team.gen.generate(random)
1519
+ // res22: Team = Team(List("b94sbz0WFC"))
1520
+ Team.gen.generate(random)
1521
+ // res23: Team = Team(List("nwyT"))
1522
+ ```
1523
+
1524
+ 4. Example 4: Recursive Tree
1525
+
1526
+ ```scala
1527
+ case class Tree(value: Int, children: List[Tree])
1528
+
1529
+ object Tree {
1530
+ implicit val schema: Schema[Tree] = Schema.derived[Tree]
1531
+ implicit val gen: Gen[Tree] = schema.derive(DeriveGen)
1532
+ }
1533
+ ```
1534
+
1535
+ The `Tree` is a record with a recursive field `children` of type `List[Tree]`. Let's see how the derived `Gen[Tree]` instance handles this recursive structure:
1536
+
1537
+ ```scala
1538
+ Tree.gen.generate(random)
1539
+ // res24: Tree = Tree(value = 1205047495, children = List())
1540
+ ```
1541
+
1542
+ 5. Example 5: DynamicValue Example
1543
+
1544
+ ```scala
1545
+ implicit val dynamicGen: Gen[DynamicValue] = Schema.dynamic.derive(DeriveGen)
1546
+ ```
1547
+
1548
+ Let's generate some random `DynamicValue` instances:
1549
+
1550
+ ```scala
1551
+ dynamicGen.generate(random)
1552
+ // res25: DynamicValue = Primitive(Int(769973518))
1553
+ dynamicGen.generate(random)
1554
+ // res26: DynamicValue = Primitive(Long(8878934151639676041L))
1555
+ dynamicGen.generate(random)
1556
+ // res27: DynamicValue = Sequence(IndexedSeq(Primitive(Int(-1576812231))))
1557
+ ```
1558
+
1559
+ 6. Example 6: Simple Email Wrapper Type
1560
+
1561
+ ```scala
1562
+ case class Email(value: String)
1563
+
1564
+ object Email {
1565
+ implicit val schema: Schema[Email] = Schema[String].transform(
1566
+ Email(_),
1567
+ _.value
1568
+ )
1569
+ implicit val gen: Gen[Email] = schema.derive(DeriveGen)
1570
+ }
1571
+ ```
1572
+
1573
+ The `Email` type is a simple wrapper around `String`. Let's see how it generates random `Email` values:
1574
+
1575
+ ```scala
1576
+ Email.gen.generate(random)
1577
+ // res28: Email = Email("zlLKVaEitt")
1578
+ Email.gen.generate(random)
1579
+ // res29: Email = Email("Sa")
1580
+ ```
1581
+
1582
+ ## Custom Type-class Instances
1583
+
1584
+ While automatic derivation generates type class instances for all substructures of a data type, there are times when you need to override the derived instance for a specific substructure. For example, you might want to use a custom `Show` instance for a particular field, provide a hand-written codec for a specific type that the deriver doesn't handle well, or inject a special implementation for testing purposes.
1585
+
1586
+ The `DerivationBuilder` provides an `instance` method that allows you to override the automatically derived type class instance for any part of the schema tree. You access the `DerivationBuilder` by calling `Schema#deriving(deriver)` instead of `Schema#derive(deriver)`:
1587
+
1588
+ ```scala
1589
+ val schema: Schema[A] = ...
1590
+ val deriver: Deriver[TC] = ...
1591
+
1592
+ // Using derive: fully automatic, no customization
1593
+ val tc: TC[A] = schema.derive(deriver)
1594
+
1595
+ // Using deriving: returns a DerivationBuilder for customization
1596
+ val tc: TC[A] = schema.deriving(deriver)
1597
+ .instance(...) // override specific instances
1598
+ .modifier(...) // override specific modifiers
1599
+ .derive // finalize the derivation
1600
+ ```
1601
+
1602
+ The `DerivationBuilder` offers two overloaded `instance` methods for providing custom type class instances:
1603
+
1604
+ ```scala
1605
+ final case class DerivationBuilder[TC[_], A](...) {
1606
+ def instance[B](optic: Optic[A, B], instance: => TC[B]): DerivationBuilder[TC, A]
1607
+ def instance[B](typeId: TypeId[B], instance: => TC[B]): DerivationBuilder[TC, A]
1608
+ }
1609
+ ```
1610
+
1611
+ ### Overriding by Optic
1612
+
1613
+ The first overload takes an `Optic[A, B]` that precisely targets a specific location within the schema tree. This is useful when you want to override the instance for a particular field or case without affecting other occurrences of the same type:
1614
+
1615
+ ```scala
1616
+ import zio.blocks.schema._
1617
+ import zio.blocks.typeid.TypeId
1618
+
1619
+ case class Person(name: String, age: Int)
1620
+
1621
+ object Person extends CompanionOptics[Person] {
1622
+ implicit val schema: Schema[Person] = Schema.derived[Person]
1623
+
1624
+ val name: Lens[Person, String] = $(_.name)
1625
+ val age: Lens[Person, Int] = $(_.age)
1626
+ }
1627
+ ```
1628
+
1629
+ Now we can override the `Show[String]` instance specifically for the `name` field of `Person`:
1630
+
1631
+ ```scala
1632
+ val customNameShow: Show[String] = new Show[String] {
1633
+ def show(value: String): String = value.toUpperCase
1634
+ }
1635
+
1636
+ val personShow: Show[Person] = Person.schema
1637
+ .deriving(DeriveShow)
1638
+ .instance(Person.name, customNameShow)
1639
+ .derive
1640
+ ```
1641
+
1642
+ When we show a `Person`, the `name` field will use the custom `Show[String]` instance (showing it in uppercase), while the `age` field will use the automatically derived `Show[Int]` instance:
1643
+
1644
+ ```scala
1645
+ personShow.show(Person("Alice", 30))
1646
+ // res30: String = "Person(name = ALICE, age = 30)"
1647
+ ```
1648
+
1649
+ You can also target deeper nested fields using composed optics. For example, if you have a `Company` that contains a `Person`, you can target the `name` field inside the nested `Person`:
1650
+
1651
+ ```scala
1652
+ case class Company(ceo: Person, industry: String)
1653
+
1654
+ object Company extends CompanionOptics[Company] {
1655
+ implicit val schema: Schema[Company] = Schema.derived[Company]
1656
+
1657
+ val ceo: Lens[Company, Person] = $(_.ceo)
1658
+ val ceoName: Lens[Company, String] = $(_.ceo.name)
1659
+ val industry: Lens[Company, String] = $(_.industry)
1660
+ }
1661
+ ```
1662
+
1663
+ ```scala
1664
+ val companyShow: Show[Company] = Company.schema
1665
+ .deriving(DeriveShow)
1666
+ .instance(Company.ceoName, customNameShow)
1667
+ .derive
1668
+ ```
1669
+
1670
+ In this case, the custom `Show[String]` instance only applies to the CEO's name. The `industry` field, which is also a `String`, will use the default derived `Show[String]` instance:
1671
+
1672
+ ```scala
1673
+ companyShow.show(Company(Person("Alice", 30), "tech"))
1674
+ // res31: String = "Company(ceo = Person(name = ALICE, age = 30), industry = \"tech\")"
1675
+ ```
1676
+
1677
+ ### Overriding by TypeId
1678
+
1679
+ The second overload takes a `TypeId[B]` and applies the custom instance to **all occurrences** of type `B` anywhere in the schema tree. This is useful when you want to override the instance for a type globally, without having to specify each location:
1680
+
1681
+ ```scala
1682
+ val customIntShow: Show[Int] = new Show[Int] {
1683
+ def show(value: Int): String = s"#$value"
1684
+ }
1685
+
1686
+ val personShow: Show[Person] = Person.schema
1687
+ .deriving(DeriveShow)
1688
+ .instance(TypeId.int, customIntShow)
1689
+ .derive
1690
+ ```
1691
+
1692
+ All `Int` fields in the `Person` schema (in this case, just `age`) will use the custom `Show[Int]` instance:
1693
+
1694
+ ```scala
1695
+ personShow.show(Person("Alice", 30))
1696
+ // res32: String = "Person(name = \"Alice\", age = #30)"
1697
+ ```
1698
+
1699
+ ### Resolution Order
1700
+
1701
+ When the derivation engine encounters a schema node, it resolves the type class instance using the following priority order:
1702
+
1703
+ 1. **Optic-based override** (most precise): If an instance override was registered using an optic that matches the current path in the schema tree, that instance is used.
1704
+ 2. **TypeId-based override** (more general): If no optic-based match is found, it checks for an instance override registered by type ID.
1705
+ 3. **Automatic derivation** (default): If no override is found, the deriver's method (e.g., `derivePrimitive`, `deriveRecord`) is called to automatically derive the instance.
1706
+
1707
+ This means you can set a global override by type and then selectively refine specific fields using optics:
1708
+
1709
+ ```scala
1710
+ val companyShow: Show[Company] = Company.schema
1711
+ .deriving(DeriveShow)
1712
+ .instance(TypeId.string, new Show[String] {
1713
+ def show(value: String): String = s"'$value'"
1714
+ })
1715
+ .instance(Company.ceoName, new Show[String] {
1716
+ def show(value: String): String = value.toUpperCase
1717
+ })
1718
+ .derive
1719
+ ```
1720
+
1721
+ In this example, all `String` fields use single quotes, except for the CEO's name which is shown in uppercase:
1722
+
1723
+ ```scala
1724
+ companyShow.show(Company(Person("Alice", 30), "tech"))
1725
+ // res33: String = "Company(ceo = Person(name = ALICE, age = 30), industry = 'tech')"
1726
+ ```
1727
+
1728
+ ### Chaining Multiple Overrides
1729
+
1730
+ The `instance` method returns a new `DerivationBuilder`, so you can chain multiple overrides fluently:
1731
+
1732
+ ```scala
1733
+ val personShow: Show[Person] = Person.schema
1734
+ .deriving(DeriveShow)
1735
+ .instance(Person.name, new Show[String] {
1736
+ def show(value: String): String = s"<<$value>>"
1737
+ })
1738
+ .instance(Person.age, new Show[Int] {
1739
+ def show(value: Int): String = s"age=$value"
1740
+ })
1741
+ .derive
1742
+ ```
1743
+
1744
+ ```scala
1745
+ personShow.show(Person("Alice", 30))
1746
+ // res34: String = "Person(name = <<Alice>>, age = age=30)"
1747
+ ```
1748
+
1749
+ ## Custom Modifiers
1750
+
1751
+ Modifiers are metadata annotations that influence how type class instances behave at runtime. For example, the `Modifier.rename` modifier tells a JSON codec to use a different field name during serialization, and `Modifier.transient` tells it to skip a field entirely.
1752
+
1753
+ While modifiers can be attached to schemas directly using Scala annotations (e.g., `@Modifier.transient`) or the `Schema#modifier` method, the `DerivationBuilder` provides a way to inject modifiers **programmatically at derivation time** without modifying the schema itself. This is particularly useful when:
1754
+
1755
+ - You don't control the schema definition (e.g., it comes from a library)
1756
+ - You need different modifiers for different derivation contexts (e.g., one JSON codec with renamed fields, another without)
1757
+ - You want to keep the schema clean and push format-specific concerns into the derivation layer
1758
+
1759
+ The `DerivationBuilder` offers two overloaded `modifier` methods:
1760
+
1761
+ ```scala
1762
+ final case class DerivationBuilder[TC[_], A](...) {
1763
+ def modifier[B](typeId: TypeId[B], modifier: Modifier.Reflect): DerivationBuilder[TC, A]
1764
+ def modifier[B](optic: Optic[A, B], modifier: Modifier) : DerivationBuilder[TC, A]
1765
+ }
1766
+ ```
1767
+
1768
+ ### Modifier Hierarchy
1769
+
1770
+ ZIO Blocks has two categories of modifiers:
1771
+
1772
+ - **`Modifier.Reflect`**: Type-level modifiers that apply to the schema node itself (e.g., `Modifier.config`).
1773
+ - **`Modifier.Term`**: Field-level or case-level modifiers that apply to a specific field of a record or case of a variant (e.g., `Modifier.transient`, `Modifier.rename`, `Modifier.alias`).
1774
+
1775
+ Note that `Modifier.config` extends both `Modifier.Term` and `Modifier.Reflect`, so it can be used at both levels.
1776
+
1777
+ ### Adding Modifiers by Optic
1778
+
1779
+ When you pass an optic and a `Modifier.Term` to the `modifier` method, the modifier is attached to the **term** (field or case) identified by the last segment of the optic path. When you pass a `Modifier.Reflect`, it is attached to the **schema node** targeted by the optic:
1780
+
1781
+ ```scala
1782
+ import zio.blocks.schema.json._
1783
+
1784
+ case class User(
1785
+ id: Long,
1786
+ name: String,
1787
+ email: String,
1788
+ internalScore: Double
1789
+ )
1790
+
1791
+ object User extends CompanionOptics[User] {
1792
+ implicit val schema: Schema[User] = Schema.derived[User]
1793
+
1794
+ val id: Lens[User, Long] = $(_.id)
1795
+ val name: Lens[User, String] = $(_.name)
1796
+ val email: Lens[User, String] = $(_.email)
1797
+ val internalScore: Lens[User, Double] = $(_.internalScore)
1798
+ }
1799
+ ```
1800
+
1801
+ Now we can derive a JSON codec with custom modifiers, renaming fields and marking one as transient, without changing the schema itself:
1802
+
1803
+ ```scala
1804
+ val jsonCodec: JsonBinaryCodec[User] = User.schema
1805
+ .deriving(JsonBinaryCodecDeriver)
1806
+ .modifier(User.name, Modifier.rename("full_name"))
1807
+ .modifier(User.email, Modifier.alias("mail"))
1808
+ .modifier(User.internalScore, Modifier.transient())
1809
+ .derive
1810
+ ```
1811
+
1812
+ In this example:
1813
+ - The `name` field will be serialized as `full_name` in JSON.
1814
+ - The `email` field will accept both `email` and `mail` as keys during deserialization.
1815
+ - The `internalScore` field will be excluded from serialization entirely.
1816
+
1817
+ ```scala
1818
+ val user = User(1L, "Alice", "alice@example.com", 95.5)
1819
+ // user: User = User(
1820
+ // id = 1L,
1821
+ // name = "Alice",
1822
+ // email = "alice@example.com",
1823
+ // internalScore = 95.5
1824
+ // )
1825
+ new String(jsonCodec.encode(user), "UTF-8")
1826
+ // res35: String = "{\"id\":1,\"full_name\":\"Alice\",\"email\":\"alice@example.com\"}"
1827
+ ```
1828
+
1829
+ ### Adding Modifiers by TypeId
1830
+
1831
+ The `modifier` method with `TypeId` allows you to add a `Modifier.Reflect` to all schema nodes of a given type. This is useful for attaching format-specific configuration metadata to all occurrences of a type:
1832
+
1833
+ ```scala
1834
+ val jsonCodec: JsonBinaryCodec[User] = User.schema
1835
+ .deriving(JsonBinaryCodecDeriver)
1836
+ .modifier(TypeId.of[User], Modifier.config("json", "camelCase"))
1837
+ .modifier(User.internalScore, Modifier.transient())
1838
+ .derive
1839
+ ```
1840
+
1841
+ ## Derivation Process In-Depth
1842
+
1843
+ Until now, we learned how to implement the `Deriver` methods for different schema patterns. But we haven't yet discussed how the overall derivation process works. In this section, we will go through the main steps of derivation in detail.
1844
+
1845
+ ### PHASE 1: Deriving the Schema for the Target Type
1846
+
1847
+ The first step in deriving a type class instance is deriving a `Schema[A]` for the target type `A`. The `Schema[A]` contains a tree of `Reflect[Binding, A]` nodes that represent the structure of `A` using structural bindings:
1848
+
1849
+ For example, assume a case class of `Person(name: String, age: Int)`. The derived schema would look like this:
1850
+
1851
+ ```
1852
+ Schema[Person]
1853
+ └── Reflect.Record[Binding, Person]
1854
+ ├── Term("name", Reflect.Primitive[Binding, String])
1855
+ └── Term("age", Reflect.Primitive[Binding, Int])
1856
+ ```
1857
+
1858
+ Each node of the derived schema tree, carries two pieces of information:
1859
+ - **Type Metadata**: Structural representation of the type (e.g., record, variant, primitive).
1860
+ - **Binding Metadata**: Structural binding information for constructing/deconstructing values of that type.
1861
+
1862
+ This schema derivation is typically done using `Schema.derived[A]`, which uses Scala's compile-time reflection capabilities to inspect the structure of type `A` and build the corresponding schema.
1863
+
1864
+ For example, the following code derives the schema for `Person`:
1865
+
1866
+ ```scala
1867
+ case class Person(name: String, age: Int)
1868
+
1869
+ object Person {
1870
+ implicit val schema: Schema[Person] = Schema.derived[Person]
1871
+ }
1872
+ ```
1873
+
1874
+ ### PHASE 2: Schema Tree Transformation
1875
+
1876
+ After generating the schema, by calling `Schema[A]#derive(deriver: Deriver[TC])`, the derivation process begins. This process involves transforming the schema tree from one that contains only structural bindings to one that also includes derived type class instances.
1877
+
1878
+ Initially, a `Schema[A]` contains `Reflect[Binding, A]` nodes that represent the structure of the type `A` using structural bindings. During derivation, the `Deriver` transforms these nodes into `Reflect[BindingInstance[TC, _, _], A]` nodes, where each node now contains both the structural binding and the derived type class instance for that part of the structure.
1879
+
1880
+ This tree transformation process starts at the root of the schema and recursively traverses each node until it reaches the leaf nodes (primitives). Now it can derive the type class instances for each leaf node by calling the `derivePrimitive` deriver method, which returns the derived type class instance wrapped in a `Lazy` container, i.e., `Lazy[TC[A]]`. The derivation builder now converts that schema node from `Reflect[Binding, A]` to `Reflect[BindingInstance[TC, _, _], A]`, where the `BindingInstance` contains both the structural binding and the derived type class instance. After converting all the leaf nodes, it backtracks up the tree, calling the appropriate `Deriver` methods for each structural pattern (record, variant, sequence, map, dynamic, wrapper) to derive type class instances for the composite types. At each step, it transforms the schema nodes from `Reflect[Binding, A]` to `Reflect[BindingInstance[TC, _, _], A]` accordingly. This process continues until it reaches the root of the schema tree, resulting in a final schema of type `Schema[A]` that contains `Reflect[BindingInstance[TC, _, _], A]` nodes throughout the entire structure.
1881
+
1882
+ The following diagram illustrates this transformation process:
1883
+
1884
+ ```
1885
+ ┌──────────────────────────────┐
1886
+ │ Reflect[Binding,A] │
1887
+ ├──────────────────────────────┤
1888
+ │ STRUCTURAL BINDING ONLY │
1889
+ └──────────────────────────────┘
1890
+ │
1891
+ │ transform
1892
+ ▼
1893
+ ┌──────────────────────────────┐
1894
+ │ Reflect[BindingInstance,A] │
1895
+ ├──────────────────────────────┤
1896
+ │ STRUCTURAL BINDING │
1897
+ │ WITH TYPE-CLASS INSTANCE │
1898
+ └──────────────────────────────┘
1899
+ │
1900
+ │ extract
1901
+ ▼
1902
+ ┌──────────────────────────────┐
1903
+ │ Lazy[TC[A]] │
1904
+ ├──────────────────────────────┤
1905
+ │ TYPE-CLASS INSTANCE │
1906
+ │ (TC[A]) │
1907
+ └──────────────────────────────┘
1908
+ ```
1909
+
1910
+ The `BindingInstance` is a container that bundles together a structural `Binding` and a derived type class instance `TC[A]`:
1911
+
1912
+ ```scala
1913
+ case class BindingInstance[TC[_], T, A](
1914
+ binding: Binding[T, A], // Original runtime binding
1915
+ instance: Lazy[TC[A]] // The derived type-class instance
1916
+ )
1917
+ ```
1918
+
1919
+ For example, the transformation sequence for the `Person` data type would look like this:
1920
+
1921
+ ```scala
1922
+ case class Person(name: String, age: Int)
1923
+
1924
+ object Person {
1925
+ implicit val schema: Schema[Person] = Schema.derived[Person]
1926
+ implicit val show: Show[Person] = schema.derive(DeriveShow)
1927
+ }
1928
+ ```
1929
+
1930
+ - Step 1: Transform Primitive "name" (String)
1931
+ - deriver.derivePrimitive(String) → Lazy[Show[String]]
1932
+ - Creating BindingInstance(Binding.Primitive, Lazy[Show[String]])
1933
+ - Converting reflect node of `String` Schema from `Reflect[Binding, String]` to `Reflect[BindingInstance, String]`
1934
+
1935
+ - Step 2: Transform Primitive "age" (Int)
1936
+ - deriver.derivePrimitive(Int) → Lazy[Show[Int]]
1937
+ - Creating BindingInstance(Binding.Primitive, Lazy[Show[Int]])
1938
+ - Converting reflect node of `Int` Schema from `Reflect[Binding, Int]` to `Reflect[BindingInstance, Int]`
1939
+
1940
+ - Step 3: Transform Record "Person"
1941
+ - deriver.deriveRecord(fields with transformed metadata) → Lazy[Show[Person]]
1942
+ - Creating BindingInstance(Binding.Record, Lazy[Show[Person]])
1943
+ - Converting reflect node of `Person` Schema from `Reflect[Binding, Person]` to `Reflect[BindingInstance, Person]`
1944
+
1945
+ ### PHASE 3: Extracting the Derived Type Class Instance
1946
+
1947
+ After the schema tree has been fully transformed to contain `Reflect[BindingInstance[TC, _, _], A]` nodes, now each node has a `BindingInstance` containing the original binding and the derived type class instance. The metadata container `BindingInstance` of the root node contains the derived type class wrapped in a `Lazy` container, i.e., `Lazy[TC[A]]`. To get the final derived type class instance, we call `force` on the `Lazy[TC[A]]`, which forces the unevaluated computation and retrieves the actual type class instance `TC[A]`.
1948
+
1949
+ ### Phase 4: Using the Derived Show Instance
1950
+
1951
+ After derivation is complete, you can use the derived type class instance as needed. For example, you can use the derived `Show[Person]` instance to display a `Person` object:
1952
+
1953
+ ```scala
1954
+ val result = Person.show.show(Person("Alice", 30))
1955
+ // result: String = "Person(name = Alice, age = 30)"
1956
+ ```
1957
+
1958
+ The interesting part here is how the `show` method of the derived `Show[Person]` instance works. It uses the `HasInstance` type class to access the derived `Show` instances for each field of the `Person` record (i.e., `Show[String]` for the `name` field and `Show[Int]` for the `age` field). This allows it to recursively display each field using its respective `Show` instance, demonstrating the composability and reusability of type class instances in the derivation system.
1959
+
1960
+ Please note that this happens when either the `Deriver` implementation uses the `HasInstance` implicit parameter or uses the centralized recursive approach to access nested derived instances.