@zio.dev/zio-blocks 0.0.21 → 0.0.22

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,1959 @@
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
+
197
+ object DeriveShow extends Deriver[Show] {
198
+
199
+ override def derivePrimitive[A](
200
+ primitiveType: PrimitiveType[A],
201
+ typeId: TypeId[A],
202
+ binding: Binding[BindingType.Primitive, A],
203
+ doc: Doc,
204
+ modifiers: Seq[Modifier.Reflect],
205
+ defaultValue: Option[A],
206
+ examples: Seq[A]
207
+ ): Lazy[Show[A]] =
208
+ Lazy {
209
+ new Show[A] {
210
+ def show(value: A): String = primitiveType match {
211
+ case _: PrimitiveType.String => "\"" + value + "\""
212
+ case _: PrimitiveType.Char => "'" + value + "'"
213
+ case _ => String.valueOf(value)
214
+ }
215
+ }
216
+ }
217
+
218
+ override def deriveRecord[F[_, _], A](
219
+ fields: IndexedSeq[Term[F, A, ?]],
220
+ typeId: TypeId[A],
221
+ binding: Binding[BindingType.Record, A],
222
+ doc: Doc,
223
+ modifiers: Seq[Modifier.Reflect],
224
+ defaultValue: Option[A],
225
+ examples: Seq[A]
226
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[A]] =
227
+ Lazy {
228
+ // Collecting Lazy[Show] instances for each field from the transformed metadata
229
+ val fieldShowInstances: IndexedSeq[(String, Lazy[Show[Any]])] = fields.map { field =>
230
+ val fieldName = field.name
231
+ // Get the Lazy[Show] instance for this field's type, but we won't force it yet
232
+ // We'll force it later when we actually need to show a value of this field
233
+ val fieldShowInstance = D.instance(field.value.metadata).asInstanceOf[Lazy[Show[Any]]]
234
+ (fieldName, fieldShowInstance)
235
+ }
236
+
237
+ // Cast fields to use Binding as F (we are going to create Reflect.Record with Binding as F)
238
+ val recordFields = fields.asInstanceOf[IndexedSeq[Term[Binding, A, ?]]]
239
+
240
+ // Cast to Binding.Record to access constructor/deconstructor
241
+ val recordBinding = binding.asInstanceOf[Binding.Record[A]]
242
+
243
+ // Build a Reflect.Record to get access to the computed registers for each field
244
+ val recordReflect = new Reflect.Record[Binding, A](recordFields, typeId, recordBinding, doc, modifiers)
245
+
246
+ new Show[A] {
247
+ def show(value: A): String = {
248
+
249
+ // Create registers with space for all used registers to hold deconstructed field values
250
+ val registers = Registers(recordReflect.usedRegisters)
251
+
252
+ // Deconstruct field values of the record into the registers
253
+ recordBinding.deconstructor.deconstruct(registers, RegisterOffset.Zero, value)
254
+
255
+ // Build string representations for all fields
256
+ val fieldStrings = fields.indices.map { i =>
257
+ val (fieldName, showInstanceLazy) = fieldShowInstances(i)
258
+ val fieldValue = recordReflect.registers(i).get(registers, RegisterOffset.Zero)
259
+ val result = s"$fieldName = ${showInstanceLazy.force.show(fieldValue)}"
260
+ result
261
+ }
262
+
263
+ s"${typeId.name}(${fieldStrings.mkString(", ")})"
264
+ }
265
+ }
266
+ }
267
+
268
+ override def deriveVariant[F[_, _], A](
269
+ cases: IndexedSeq[Term[F, A, ?]],
270
+ typeId: TypeId[A],
271
+ binding: Binding[BindingType.Variant, A],
272
+ doc: Doc,
273
+ modifiers: Seq[Modifier.Reflect],
274
+ defaultValue: Option[A],
275
+ examples: Seq[A]
276
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[A]] = Lazy {
277
+ // Get Show instances for all cases LAZILY
278
+ val caseShowInstances: IndexedSeq[Lazy[Show[Any]]] = cases.map { case_ =>
279
+ D.instance(case_.value.metadata).asInstanceOf[Lazy[Show[Any]]]
280
+ }
281
+
282
+ // Cast binding to Binding.Variant to access discriminator and matchers
283
+ val variantBinding = binding.asInstanceOf[Binding.Variant[A]]
284
+ val discriminator = variantBinding.discriminator
285
+ val matchers = variantBinding.matchers
286
+
287
+ new Show[A] {
288
+ // Implement show by using discriminator and matchers to find the right case
289
+ // The `value` parameter is of type A (the variant type), e.g. an Option[Int] value
290
+ def show(value: A): String = {
291
+ // Use discriminator to determine which case this value belongs to
292
+ val caseIndex = discriminator.discriminate(value)
293
+
294
+ // Use matcher to downcast to the specific case type
295
+ val caseValue = matchers(caseIndex).downcastOrNull(value)
296
+
297
+ // Just delegate to the case's Show instance - it already knows its own name
298
+ caseShowInstances(caseIndex).force.show(caseValue)
299
+ }
300
+ }
301
+ }
302
+
303
+ override def deriveSequence[F[_, _], C[_], A](
304
+ element: Reflect[F, A],
305
+ typeId: TypeId[C[A]],
306
+ binding: Binding[BindingType.Seq[C], C[A]],
307
+ doc: Doc,
308
+ modifiers: Seq[Modifier.Reflect],
309
+ defaultValue: Option[C[A]],
310
+ examples: Seq[C[A]]
311
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[C[A]]] = Lazy {
312
+ // Get Show instance for element type LAZILY
313
+ val elementShowLazy: Lazy[Show[A]] = D.instance(element.metadata)
314
+
315
+ // Cast binding to Binding.Seq to access the deconstructor
316
+ val seqBinding = binding.asInstanceOf[Binding.Seq[C, A]]
317
+ val deconstructor = seqBinding.deconstructor
318
+
319
+ new Show[C[A]] {
320
+ def show(value: C[A]): String = {
321
+ // Use deconstructor to iterate over elements
322
+ val iterator = deconstructor.deconstruct(value)
323
+ // Force the element Show instance only when actually showing
324
+ val elements = iterator.map(elem => elementShowLazy.force.show(elem)).mkString(", ")
325
+ s"[$elements]"
326
+ }
327
+ }
328
+ }
329
+
330
+ override def deriveMap[F[_, _], M[_, _], K, V](
331
+ key: Reflect[F, K],
332
+ value: Reflect[F, V],
333
+ typeId: TypeId[M[K, V]],
334
+ binding: Binding[BindingType.Map[M], M[K, V]],
335
+ doc: Doc,
336
+ modifiers: Seq[Modifier.Reflect],
337
+ defaultValue: Option[M[K, V]],
338
+ examples: Seq[M[K, V]]
339
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[M[K, V]]] = Lazy {
340
+ // Get Show instances for key and value types LAZILY
341
+ val keyShowLazy: Lazy[Show[K]] = D.instance(key.metadata)
342
+ val valueShowLazy: Lazy[Show[V]] = D.instance(value.metadata)
343
+
344
+ // Cast binding to Binding.Map to access the deconstructor
345
+ val mapBinding = binding.asInstanceOf[Binding.Map[M, K, V]]
346
+ val deconstructor = mapBinding.deconstructor
347
+
348
+ new Show[M[K, V]] {
349
+ def show(m: M[K, V]): String = {
350
+ // Use deconstructor to iterate over key-value pairs
351
+ val iterator = deconstructor.deconstruct(m)
352
+ // Force the Show instances only when actually showing
353
+ val entries = iterator.map { kv =>
354
+ val k = deconstructor.getKey(kv)
355
+ val v = deconstructor.getValue(kv)
356
+ s"${keyShowLazy.force.show(k)} -> ${valueShowLazy.force.show(v)}"
357
+ }.mkString(", ")
358
+ s"Map($entries)"
359
+ }
360
+ }
361
+ }
362
+
363
+ override def deriveDynamic[F[_, _]](
364
+ binding: Binding[BindingType.Dynamic, DynamicValue],
365
+ doc: Doc,
366
+ modifiers: Seq[Modifier.Reflect],
367
+ defaultValue: Option[DynamicValue],
368
+ examples: Seq[DynamicValue]
369
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[DynamicValue]] = Lazy {
370
+ new Show[DynamicValue] {
371
+ def show(value: DynamicValue): String =
372
+ value match {
373
+ case DynamicValue.Primitive(pv) =>
374
+ value.toString
375
+
376
+ case DynamicValue.Record(fields) =>
377
+ val fieldStrings = fields.map { case (name, v) =>
378
+ s"$name = ${show(v)}"
379
+ }
380
+ s"Record(${fieldStrings.mkString(", ")})"
381
+
382
+ case DynamicValue.Variant(caseName, v) =>
383
+ s"$caseName(${show(v)})"
384
+
385
+ case DynamicValue.Sequence(elements) =>
386
+ val elemStrings = elements.map(show)
387
+ s"[${elemStrings.mkString(", ")}]"
388
+
389
+ case DynamicValue.Map(entries) =>
390
+ val entryStrings = entries.map { case (k, v) =>
391
+ s"${show(k)} -> ${show(v)}"
392
+ }
393
+ s"Map(${entryStrings.mkString(", ")})"
394
+ case Null =>
395
+ "null"
396
+ }
397
+ }
398
+ }
399
+
400
+ override def deriveWrapper[F[_, _], A, B](
401
+ wrapped: Reflect[F, B],
402
+ typeId: TypeId[A],
403
+ binding: Binding[BindingType.Wrapper[A, B], A],
404
+ doc: Doc,
405
+ modifiers: Seq[Modifier.Reflect],
406
+ defaultValue: Option[A],
407
+ examples: Seq[A]
408
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[A]] = Lazy {
409
+ // Get Show instance for the wrapped (underlying) type B LAZILY
410
+ val wrappedShowLazy: Lazy[Show[B]] = D.instance(wrapped.metadata)
411
+
412
+ // Cast binding to Binding.Wrapper to access unwrap function
413
+ val wrapperBinding = binding.asInstanceOf[Binding.Wrapper[A, B]]
414
+
415
+ new Show[A] {
416
+ def show(value: A): String = {
417
+ val unwrapped = wrapperBinding.unwrap(value)
418
+ s"${typeId.name}(${wrappedShowLazy.force.show(unwrapped)})"
419
+ }
420
+ }
421
+ }
422
+ }
423
+ ```
424
+
425
+ Now let's see how the derivation process works step by step.
426
+
427
+ ### Primitive Derivation
428
+
429
+ 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:
430
+
431
+
432
+ ```scala
433
+ def derivePrimitive[A](
434
+ primitiveType: PrimitiveType[A],
435
+ typeId: TypeId[A],
436
+ binding: Binding[BindingType.Primitive, A],
437
+ doc: Doc,
438
+ modifiers: Seq[Modifier.Reflect],
439
+ defaultValue: Option[A],
440
+ examples: Seq[A]
441
+ ): Lazy[Show[A]] =
442
+ Lazy {
443
+ new Show[A] {
444
+ def show(value: A): String = primitiveType match {
445
+ case _: PrimitiveType.String => "\"" + value + "\""
446
+ case _: PrimitiveType.Char => "'" + value + "'"
447
+ case _ => String.valueOf(value)
448
+ }
449
+ }
450
+ }
451
+ ```
452
+
453
+ 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.
454
+
455
+ 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.
456
+
457
+ ### Record Derivation
458
+
459
+ 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:
460
+
461
+
462
+ ```scala
463
+ def deriveRecord[F[_, _], A](
464
+ fields: IndexedSeq[Term[F, A, ?]],
465
+ typeId: TypeId[A],
466
+ binding: Binding[BindingType.Record, A],
467
+ doc: Doc,
468
+ modifiers: Seq[Modifier.Reflect],
469
+ defaultValue: Option[A],
470
+ examples: Seq[A]
471
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[A]] =
472
+ Lazy {
473
+ // Collecting Lazy[Show] instances for each field from the transformed metadata
474
+ val fieldShowInstances: IndexedSeq[(String, Lazy[Show[Any]])] = fields.map { field =>
475
+ val fieldName = field.name
476
+ // Get the Lazy[Show] instance for this field's type, but we won't force it yet
477
+ // We'll force it later when we actually need to show a value of this field
478
+ val fieldShowInstance = D.instance(field.value.metadata).asInstanceOf[Lazy[Show[Any]]]
479
+ (fieldName, fieldShowInstance)
480
+ }
481
+
482
+ // Cast fields to use Binding as F (we are going to create Reflect.Record with Binding as F)
483
+ val recordFields = fields.asInstanceOf[IndexedSeq[Term[Binding, A, ?]]]
484
+
485
+ // Cast to Binding.Record to access constructor/deconstructor
486
+ val recordBinding = binding.asInstanceOf[Binding.Record[A]]
487
+
488
+ // Build a Reflect.Record to get access to the computed registers for each field
489
+ val recordReflect = new Reflect.Record[Binding, A](recordFields, typeId, recordBinding, doc, modifiers)
490
+
491
+ new Show[A] {
492
+ def show(value: A): String = {
493
+
494
+ // Create registers with space for all used registers to hold deconstructed field values
495
+ val registers = Registers(recordReflect.usedRegisters)
496
+
497
+ // Deconstruct field values of the record into the registers
498
+ recordBinding.deconstructor.deconstruct(registers, RegisterOffset.Zero, value)
499
+
500
+ // Build string representations for all fields
501
+ val fieldStrings = fields.indices.map { i =>
502
+ val (fieldName, showInstanceLazy) = fieldShowInstances(i)
503
+ val fieldValue = recordReflect.registers(i).get(registers, RegisterOffset.Zero)
504
+ val result = s"$fieldName = ${showInstanceLazy.force.show(fieldValue)}"
505
+ result
506
+ }
507
+
508
+ s"${typeId.name}(${fieldStrings.mkString(", ")})"
509
+ }
510
+ }
511
+ }
512
+ ```
513
+
514
+ 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:
515
+ 1. First, we extract the type class instances for each field of the record.
516
+ 2. Second, we have to deconstruct the record value at runtime to access individual field values.
517
+ 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.
518
+
519
+ 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.
520
+
521
+ 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.
522
+
523
+ 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.
524
+
525
+ 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.
526
+
527
+ 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`.
528
+
529
+ 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`.
530
+
531
+ 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.
532
+
533
+ ### Variant Derivation
534
+
535
+ 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:
536
+
537
+ ```scala
538
+ def deriveVariant[F[_, _], A](
539
+ cases: IndexedSeq[Term[F, A, ?]],
540
+ typeId: TypeId[A],
541
+ binding: Binding[BindingType.Variant, A],
542
+ doc: Doc,
543
+ modifiers: Seq[Modifier.Reflect],
544
+ defaultValue: Option[A],
545
+ examples: Seq[A]
546
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[A]] = Lazy {
547
+ // Get Show instances for all cases LAZILY
548
+ val caseShowInstances: IndexedSeq[Lazy[Show[Any]]] = cases.map { case_ =>
549
+ D.instance(case_.value.metadata).asInstanceOf[Lazy[Show[Any]]]
550
+ }
551
+ // Cast binding to Binding.Variant to access discriminator and matchers
552
+ val variantBinding = binding.asInstanceOf[Binding.Variant[A]]
553
+ val discriminator = variantBinding.discriminator
554
+ val matchers = variantBinding.matchers
555
+ new Show[A] {
556
+ // Implement show by using discriminator and matchers to find the right case
557
+ // The `value` parameter is of type A (the variant type), e.g. an Option[Int] value
558
+ def show(value: A): String = {
559
+ // Use discriminator to determine which case this value belongs to
560
+ val caseIndex = discriminator.discriminate(value)
561
+ // Use matcher to downcast to the specific case type
562
+ val caseValue = matchers(caseIndex).downcastOrNull(value)
563
+ // Just delegate to the case's Show instance - it already knows its own name
564
+ caseShowInstances(caseIndex).force.show(caseValue)
565
+ }
566
+ }
567
+ }
568
+ ```
569
+
570
+ 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.
571
+
572
+ 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.
573
+
574
+ ### Sequence Derivation
575
+
576
+ 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:
577
+
578
+ ```scala
579
+ def deriveSequence[F[_, _], C[_], A](
580
+ element: Reflect[F, A],
581
+ typeId: TypeId[C[A]],
582
+ binding: Binding[BindingType.Seq[C], C[A]],
583
+ doc: Doc,
584
+ modifiers: Seq[Modifier.Reflect],
585
+ defaultValue: Option[C[A]],
586
+ examples: Seq[C[A]]
587
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[C[A]]] = Lazy {
588
+ // Get Show instance for element type (lazily)
589
+ val elementShowLazy: Lazy[Show[A]] = D.instance(element.metadata)
590
+ // Cast binding to Binding.Seq to access the deconstructor
591
+ val seqBinding = binding.asInstanceOf[Binding.Seq[C, A]]
592
+ val deconstructor = seqBinding.deconstructor
593
+ new Show[C[A]] {
594
+ def show(value: C[A]): String = {
595
+ // Use the deconstructor to iterate over elements
596
+ val iterator = deconstructor.deconstruct(value)
597
+ // Force the element Show instance only when actually showing
598
+ val elements = iterator.map(elem => elementShowLazy.force.show(elem)).mkString(", ")
599
+ s"[$elements]"
600
+ }
601
+ }
602
+ }
603
+ ```
604
+
605
+ 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.
606
+
607
+ ### Map Derivation
608
+
609
+ 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:
610
+
611
+ ```scala
612
+ def deriveMap[F[_, _], M[_, _], K, V](
613
+ key: Reflect[F, K],
614
+ value: Reflect[F, V],
615
+ typeId: TypeId[M[K, V]],
616
+ binding: Binding[BindingType.Map[M], M[K, V]],
617
+ doc: Doc,
618
+ modifiers: Seq[Modifier.Reflect],
619
+ defaultValue: Option[M[K, V]],
620
+ examples: Seq[M[K, V]]
621
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[M[K, V]]] = Lazy {
622
+ // Get Show instances for key and value types LAZILY
623
+ val keyShowLazy: Lazy[Show[K]] = D.instance(key.metadata)
624
+ val valueShowLazy: Lazy[Show[V]] = D.instance(value.metadata)
625
+
626
+ // Cast binding to Binding.Map to access the deconstructor
627
+ val mapBinding = binding.asInstanceOf[Binding.Map[M, K, V]]
628
+ val deconstructor = mapBinding.deconstructor
629
+
630
+ new Show[M[K, V]] {
631
+ def show(m: M[K, V]): String = {
632
+ // Use deconstructor to iterate over key-value pairs
633
+ val iterator = deconstructor.deconstruct(m)
634
+ // Force the Show instances only when actually showing
635
+ val entries = iterator.map { kv =>
636
+ val k = deconstructor.getKey(kv)
637
+ val v = deconstructor.getValue(kv)
638
+ s"${keyShowLazy.force.show(k)} -> ${valueShowLazy.force.show(v)}"
639
+ }.mkString(", ")
640
+ s"Map($entries)"
641
+ }
642
+ }
643
+ }
644
+ ```
645
+
646
+ 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.
647
+
648
+ ### Dynamic Derivation
649
+
650
+ 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:
651
+
652
+ ```scala
653
+ def deriveDynamic[F[_, _]](
654
+ binding: Binding[BindingType.Dynamic, DynamicValue],
655
+ doc: Doc,
656
+ modifiers: Seq[Modifier.Reflect],
657
+ defaultValue: Option[DynamicValue],
658
+ examples: Seq[DynamicValue]
659
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[DynamicValue]] = Lazy {
660
+ new Show[DynamicValue] {
661
+ def show(value: DynamicValue): String =
662
+ value match {
663
+ case DynamicValue.Primitive(pv) =>
664
+ value.toString
665
+ case DynamicValue.Record(fields) =>
666
+ val fieldStrings = fields.map { case (name, v) =>
667
+ s"$name = ${show(v)}"
668
+ }
669
+ s"Record(${fieldStrings.mkString(", ")})"
670
+ case DynamicValue.Variant(caseName, v) =>
671
+ s"$caseName(${show(v)})"
672
+ case DynamicValue.Sequence(elements) =>
673
+ val elemStrings = elements.map(show)
674
+ s"[${elemStrings.mkString(", ")}]"
675
+ case DynamicValue.Map(entries) =>
676
+ val entryStrings = entries.map { case (k, v) =>
677
+ s"${show(k)} -> ${show(v)}"
678
+ }
679
+ s"Map(${entryStrings.mkString(", ")})"
680
+ case Null =>
681
+ "null"
682
+ }
683
+ }
684
+ }
685
+ ```
686
+
687
+ 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".
688
+
689
+ ### Wrapper Derivation
690
+
691
+ 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:
692
+
693
+ ```scala
694
+ def deriveWrapper[F[_, _], A, B](
695
+ wrapped: Reflect[F, B],
696
+ typeId: TypeId[A],
697
+ binding: Binding[BindingType.Wrapper[A, B], A],
698
+ doc: Doc,
699
+ modifiers: Seq[Modifier.Reflect],
700
+ defaultValue: Option[A],
701
+ examples: Seq[A]
702
+ )(implicit F: HasBinding[F], D: DeriveShow.HasInstance[F]): Lazy[Show[A]] = Lazy {
703
+ // Get Show instance for the wrapped (underlying) type B LAZILY
704
+ val wrappedShowLazy: Lazy[Show[B]] = D.instance(wrapped.metadata)
705
+
706
+ // Cast binding to Binding.Wrapper to access unwrap function
707
+ val wrapperBinding = binding.asInstanceOf[Binding.Wrapper[A, B]]
708
+
709
+ new Show[A] {
710
+ def show(value: A): String = {
711
+ val unwrapped = wrapperBinding.unwrap(value)
712
+ s"${typeId.name}(${wrappedShowLazy.force.show(unwrapped)})"
713
+ }
714
+ }
715
+ }
716
+ ```
717
+
718
+ 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.
719
+
720
+ ### Example Usages
721
+
722
+ 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.
723
+
724
+ 1. Example 1: Simple `Person` Record with Two Primitive Fields:
725
+
726
+ ```scala
727
+ case class Person(name: String, age: Int)
728
+
729
+ object Person {
730
+ implicit val schema: Schema[Person] = Schema.derived[Person]
731
+ implicit val show: Show[Person] = schema.derive(DeriveShow)
732
+ }
733
+ ```
734
+
735
+ Now we can use the derived `Show[Person]` instance to convert `Person` values to strings:
736
+
737
+ ```scala
738
+ Person.show.show(Person("Alice", 30))
739
+ // res7: String = "Person(name = \"Alice\", age = 30)"
740
+ ```
741
+
742
+ 2. Simple Shape Variant (Circle, Rectangle)
743
+
744
+ ```scala
745
+ sealed trait Shape
746
+ case class Circle(radius: Double) extends Shape
747
+ case class Rectangle(width: Double, height: Double) extends Shape
748
+
749
+ object Shape {
750
+ implicit val schema: Schema[Shape] = Schema.derived[Shape]
751
+ implicit val show: Show[Shape] = schema.derive(DeriveShow)
752
+ }
753
+ ```
754
+
755
+ To show a `Shape` value, we can do the following:
756
+
757
+ ```scala
758
+ val shape1: Shape = Circle(5.0)
759
+ // shape1: Shape = Circle(5.0)
760
+ Shape.show.show(shape1)
761
+ // res8: String = "Circle(radius = 5.0)"
762
+
763
+ val shape2: Shape = Rectangle(4.0, 6.0)
764
+ // shape2: Shape = Rectangle(width = 4.0, height = 6.0)
765
+ Shape.show.show(shape2)
766
+ // res9: String = "Rectangle(width = 4.0, height = 6.0)"
767
+ ```
768
+
769
+ 3. Recursive Tree and Expr
770
+
771
+ ```scala
772
+ case class Tree(value: Int, children: List[Tree])
773
+ object Tree {
774
+ implicit val schema: Schema[Tree] = Schema.derived[Tree]
775
+ implicit val show: Show[Tree] = schema.derive(DeriveShow)
776
+ }
777
+ ```
778
+
779
+ 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:
780
+
781
+ ```scala
782
+ val tree = Tree(1, List(Tree(2, List(Tree(4, Nil))), Tree(3, Nil)))
783
+ // tree: Tree = Tree(
784
+ // value = 1,
785
+ // children = List(
786
+ // Tree(value = 2, children = List(Tree(value = 4, children = List()))),
787
+ // Tree(value = 3, children = List())
788
+ // )
789
+ // )
790
+ Tree.show.show(tree)
791
+ // res10: String = "Tree(value = 1, children = [Tree(value = 2, children = [Tree(value = 4, children = [])]), Tree(value = 3, children = [])])"
792
+ ```
793
+
794
+ 4. Example 4: Recursive Sealed Trait (Expr)
795
+
796
+ ```scala
797
+ sealed trait Expr
798
+ case class Num(n: Int) extends Expr
799
+ case class Add(a: Expr, b: Expr) extends Expr
800
+
801
+ object Expr {
802
+ implicit val schema: Schema[Expr] = Schema.derived[Expr]
803
+ implicit val show: Show[Expr] = schema.derive(DeriveShow)
804
+ }
805
+ ```
806
+
807
+ Similar to `Tree`, `Expr` is a recursive variant type. The derived `Show[Expr]` instance can handle this recursive structure as well:
808
+
809
+ ```scala
810
+ val expr: Expr = Add(Num(1), Add(Num(2), Num(3)))
811
+ // expr: Expr = Add(a = Num(1), b = Add(a = Num(2), b = Num(3)))
812
+ Expr.show.show(expr)
813
+ // res11: String = "Add(a = Num(n = 1), b = Add(a = Num(n = 2), b = Num(n = 3)))"
814
+ ```
815
+
816
+ 5. Example 5: DynamicValue Example
817
+
818
+ ```scala
819
+ implicit val dynamicShow: Show[DynamicValue] = Schema.dynamic.derive(DeriveShow)
820
+ ```
821
+
822
+ 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:
823
+
824
+ ```scala
825
+ val manualRecord = DynamicValue.Record(
826
+ Chunk(
827
+ "id" -> DynamicValue.Primitive(PrimitiveValue.Int(42)),
828
+ "title" -> DynamicValue.Primitive(PrimitiveValue.String("Hello World")),
829
+ "tags" -> DynamicValue.Sequence(
830
+ Chunk(
831
+ DynamicValue.Primitive(PrimitiveValue.String("scala")),
832
+ DynamicValue.Primitive(PrimitiveValue.String("zio"))
833
+ )
834
+ )
835
+ )
836
+ )
837
+ // manualRecord: Record = Record(
838
+ // IndexedSeq(
839
+ // ("id", Primitive(Int(42))),
840
+ // ("title", Primitive(String("Hello World"))),
841
+ // (
842
+ // "tags",
843
+ // Sequence(IndexedSeq(Primitive(String("scala")), Primitive(String("zio"))))
844
+ // )
845
+ // )
846
+ // )
847
+
848
+ dynamicShow.show(manualRecord)
849
+ // res12: String = "Record(id = 42, title = \"Hello World\", tags = [\"scala\", \"zio\"])"
850
+ ```
851
+
852
+ 6. Example 6: Simple Email Wrapper Type
853
+
854
+ ```scala
855
+ case class Email(value: String)
856
+ object Email {
857
+ implicit val schema: Schema[Email] = Schema[String].transform(
858
+ Email(_),
859
+ _.value
860
+ )
861
+ implicit val show: Show[Email] = schema.derive(DeriveShow)
862
+ }
863
+ ```
864
+
865
+ The `Email` type is a simple wrapper around `String`. Let's see how it shows an `Email` value:
866
+
867
+ ```scala
868
+ val email = Email("alice@example.com")
869
+ // email: Email = Email("alice@example.com")
870
+ println(s"Email: ${Email.show.show(email)}")
871
+ // Email: Email("alice@example.com")
872
+ ```
873
+
874
+ ## Example 2: Deriving a `Gen` Type Class Instance
875
+
876
+ Let's say we want to derive a `Gen` type class instance for any type `A`:
877
+
878
+ ```scala
879
+ import scala.util.Random
880
+
881
+ trait Gen[A] {
882
+ def generate(random: Random): A
883
+ }
884
+ ```
885
+
886
+ 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`.
887
+
888
+ 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.
889
+
890
+ Here is a simple pedagogical implementation of a `GenDeriver` that can derive `Gen` instances for various types:
891
+
892
+ ```scala
893
+ import zio.blocks.chunk.Chunk
894
+ import zio.blocks.schema.*
895
+ import zio.blocks.schema.binding.*
896
+ import zio.blocks.schema.derive.Deriver
897
+ import zio.blocks.typeid.TypeId
898
+
899
+ object DeriveGen extends Deriver[Gen] {
900
+
901
+ override def derivePrimitive[A](
902
+ primitiveType: PrimitiveType[A],
903
+ typeId: TypeId[A],
904
+ binding: Binding[BindingType.Primitive, A],
905
+ doc: Doc,
906
+ modifiers: Seq[Modifier.Reflect],
907
+ defaultValue: Option[A],
908
+ examples: Seq[A]
909
+ ): Lazy[Gen[A]] =
910
+ Lazy {
911
+ new Gen[A] {
912
+ def generate(random: Random): A = primitiveType match {
913
+ case _: PrimitiveType.String => random.alphanumeric.take(random.nextInt(10) + 1).mkString.asInstanceOf[A]
914
+ case _: PrimitiveType.Char => random.alphanumeric.head.asInstanceOf[A]
915
+ case _: PrimitiveType.Boolean => random.nextBoolean().asInstanceOf[A]
916
+ case _: PrimitiveType.Int => random.nextInt().asInstanceOf[A]
917
+ case _: PrimitiveType.Long => random.nextLong().asInstanceOf[A]
918
+ case _: PrimitiveType.Double => random.nextDouble().asInstanceOf[A]
919
+ case PrimitiveType.Unit => ().asInstanceOf[A]
920
+ // For brevity, other primitives default to their zero/empty value
921
+ // In a real implementation, you'd want to handle all primitives and possibly use modifiers for ranges, etc.
922
+ case _ =>
923
+ defaultValue.getOrElse {
924
+ throw new IllegalArgumentException(
925
+ s"Gen derivation not implemented for primitive type $primitiveType " +
926
+ s"(typeId = $typeId) and no default value provided."
927
+ )
928
+ }
929
+ }
930
+ }
931
+ }
932
+
933
+ /**
934
+ * Strategy:
935
+ * 1. Get Gen type class instances for each field
936
+ * 2. Generate random values for each field
937
+ * 3. Use the constructor to build the record
938
+ */
939
+ override def deriveRecord[F[_, _], A](
940
+ fields: IndexedSeq[Term[F, A, ?]],
941
+ typeId: TypeId[A],
942
+ binding: Binding[BindingType.Record, A],
943
+ doc: Doc,
944
+ modifiers: Seq[Modifier.Reflect],
945
+ defaultValue: Option[A],
946
+ examples: Seq[A]
947
+ )(implicit F: HasBinding[F], D: DeriveGen.HasInstance[F]): Lazy[Gen[A]] =
948
+ Lazy {
949
+ // Get Gen instances for each field
950
+ val fieldGens: IndexedSeq[Lazy[Gen[Any]]] = fields.map { field =>
951
+ D.instance(field.value.metadata).asInstanceOf[Lazy[Gen[Any]]]
952
+ }
953
+
954
+ // Build Reflect.Record to access registers and constructor
955
+ val recordFields = fields.asInstanceOf[IndexedSeq[Term[Binding, A, ?]]]
956
+ val recordBinding = binding.asInstanceOf[Binding.Record[A]]
957
+ val recordReflect = new Reflect.Record[Binding, A](recordFields, typeId, recordBinding, doc, modifiers)
958
+
959
+ new Gen[A] {
960
+ def generate(random: Random): A = {
961
+ // Create registers to hold field values
962
+ val registers = Registers(recordReflect.usedRegisters)
963
+
964
+ // Generate each field and store in registers
965
+ fields.indices.foreach { i =>
966
+ val value = fieldGens(i).force.generate(random)
967
+ recordReflect.registers(i).set(registers, RegisterOffset.Zero, value)
968
+ }
969
+
970
+ // Construct the record from registers
971
+ recordBinding.constructor.construct(registers, RegisterOffset.Zero)
972
+ }
973
+ }
974
+ }
975
+
976
+ /**
977
+ * Strategy:
978
+ * 1. Get Gen type class instances for all cases
979
+ * 2. Randomly pick a case
980
+ * 3. Generate a value for that case
981
+ */
982
+ override def deriveVariant[F[_, _], A](
983
+ cases: IndexedSeq[Term[F, A, ?]],
984
+ typeId: TypeId[A],
985
+ binding: Binding[BindingType.Variant, A],
986
+ doc: Doc,
987
+ modifiers: Seq[Modifier.Reflect],
988
+ defaultValue: Option[A],
989
+ examples: Seq[A]
990
+ )(implicit F: HasBinding[F], D: DeriveGen.HasInstance[F]): Lazy[Gen[A]] = Lazy {
991
+ // Get Gen instances for all cases
992
+ val caseGens: IndexedSeq[Lazy[Gen[A]]] = cases.map { c =>
993
+ D.instance(c.value.metadata).asInstanceOf[Lazy[Gen[A]]]
994
+ }
995
+
996
+ new Gen[A] {
997
+ def generate(random: Random): A = {
998
+ // Pick a random case and generate its value
999
+ val caseIndex = random.nextInt(cases.length)
1000
+ caseGens(caseIndex).force.generate(random)
1001
+ }
1002
+ }
1003
+ }
1004
+
1005
+ /**
1006
+ * Strategy:
1007
+ * 1. Get Gen type class instances for the element type
1008
+ * 2. Generate 0-5 elements
1009
+ * 3. Build the collection using the constructor
1010
+ */
1011
+ override def deriveSequence[F[_, _], C[_], A](
1012
+ element: Reflect[F, A],
1013
+ typeId: TypeId[C[A]],
1014
+ binding: Binding[BindingType.Seq[C], C[A]],
1015
+ doc: Doc,
1016
+ modifiers: Seq[Modifier.Reflect],
1017
+ defaultValue: Option[C[A]],
1018
+ examples: Seq[C[A]]
1019
+ )(implicit F: HasBinding[F], D: DeriveGen.HasInstance[F]): Lazy[Gen[C[A]]] = Lazy {
1020
+ val elementGen = D.instance(element.metadata)
1021
+ val seqBinding = binding.asInstanceOf[Binding.Seq[C, A]]
1022
+ val constructor = seqBinding.constructor
1023
+
1024
+ new Gen[C[A]] {
1025
+ def generate(random: Random): C[A] = {
1026
+ val length = random.nextInt(6) // 0 to 5 elements
1027
+ implicit val ct: scala.reflect.ClassTag[A] = scala.reflect.ClassTag.Any.asInstanceOf[scala.reflect.ClassTag[A]]
1028
+
1029
+ if (length == 0) {
1030
+ constructor.empty[A]
1031
+ } else {
1032
+ val builder = constructor.newBuilder[A](length)
1033
+ (0 until length).foreach { _ =>
1034
+ constructor.add(builder, elementGen.force.generate(random))
1035
+ }
1036
+ constructor.result(builder)
1037
+ }
1038
+ }
1039
+ }
1040
+ }
1041
+
1042
+ /**
1043
+ * Strategy:
1044
+ * 1. Get Gen type class instances for key and value types
1045
+ * 2. Generate 0-5 key-value pairs
1046
+ * 3. Build the map using the constructor
1047
+ */
1048
+ override def deriveMap[F[_, _], M[_, _], K, V](
1049
+ key: Reflect[F, K],
1050
+ value: Reflect[F, V],
1051
+ typeId: TypeId[M[K, V]],
1052
+ binding: Binding[BindingType.Map[M], M[K, V]],
1053
+ doc: Doc,
1054
+ modifiers: Seq[Modifier.Reflect],
1055
+ defaultValue: Option[M[K, V]],
1056
+ examples: Seq[M[K, V]]
1057
+ )(implicit F: HasBinding[F], D: DeriveGen.HasInstance[F]): Lazy[Gen[M[K, V]]] = Lazy {
1058
+ val keyGen = D.instance(key.metadata)
1059
+ val valueGen = D.instance(value.metadata)
1060
+ val mapBinding = binding.asInstanceOf[Binding.Map[M, K, V]]
1061
+ val constructor = mapBinding.constructor
1062
+
1063
+ new Gen[M[K, V]] {
1064
+ def generate(random: Random): M[K, V] = {
1065
+ val size = random.nextInt(6) // 0 to 5 entries
1066
+
1067
+ if (size == 0) {
1068
+ constructor.emptyObject[K, V]
1069
+ } else {
1070
+ val builder = constructor.newObjectBuilder[K, V](size)
1071
+ (0 until size).foreach { _ =>
1072
+ constructor.addObject(builder, keyGen.force.generate(random), valueGen.force.generate(random))
1073
+ }
1074
+ constructor.resultObject(builder)
1075
+ }
1076
+ }
1077
+ }
1078
+ }
1079
+
1080
+ /**
1081
+ * Since DynamicValue can represent any schema type, we generate random
1082
+ * dynamic values by randomly choosing a variant and generating appropriate
1083
+ * content.
1084
+ */
1085
+ override def deriveDynamic[F[_, _]](
1086
+ binding: Binding[BindingType.Dynamic, DynamicValue],
1087
+ doc: Doc,
1088
+ modifiers: Seq[Modifier.Reflect],
1089
+ defaultValue: Option[DynamicValue],
1090
+ examples: Seq[DynamicValue]
1091
+ )(implicit F: HasBinding[F], D: DeriveGen.HasInstance[F]): Lazy[Gen[DynamicValue]] = Lazy {
1092
+ new Gen[DynamicValue] {
1093
+ // Helper to generate a random primitive value
1094
+ private def randomPrimitive(random: Random): DynamicValue.Primitive = {
1095
+ val primitiveType = random.nextInt(5)
1096
+ primitiveType match {
1097
+ case 0 => DynamicValue.Primitive(PrimitiveValue.Int(random.nextInt()))
1098
+ case 1 => DynamicValue.Primitive(PrimitiveValue.String(random.alphanumeric.take(10).mkString))
1099
+ case 2 => DynamicValue.Primitive(PrimitiveValue.Boolean(random.nextBoolean()))
1100
+ case 3 => DynamicValue.Primitive(PrimitiveValue.Double(random.nextDouble()))
1101
+ case 4 => DynamicValue.Primitive(PrimitiveValue.Long(random.nextLong()))
1102
+ }
1103
+ }
1104
+
1105
+ def generate(random: Random): DynamicValue = {
1106
+ // Randomly choose what kind of DynamicValue to generate
1107
+ // Weight towards primitives and simpler structures to avoid deep nesting
1108
+ val valueType = random.nextInt(10)
1109
+ valueType match {
1110
+ case 0 | 1 | 2 | 3 | 4 =>
1111
+ // 50% chance: generate a primitive
1112
+ randomPrimitive(random)
1113
+
1114
+ case 5 | 6 =>
1115
+ // 20% chance: generate a record with 1-3 fields
1116
+ val numFields = random.nextInt(3) + 1
1117
+ val fields = (0 until numFields).map { i =>
1118
+ val fieldName = s"field$i"
1119
+ val fieldValue = randomPrimitive(random)
1120
+ (fieldName, fieldValue: DynamicValue)
1121
+ }
1122
+ DynamicValue.Record(Chunk.from(fields))
1123
+
1124
+ case 7 | 8 =>
1125
+ // 20% chance: generate a sequence of 0-3 primitives
1126
+ val numElements = random.nextInt(4)
1127
+ val elements = (0 until numElements).map(_ => randomPrimitive(random): DynamicValue)
1128
+ DynamicValue.Sequence(Chunk.from(elements))
1129
+
1130
+ case 9 =>
1131
+ // 10% chance: generate null
1132
+ DynamicValue.Null
1133
+ }
1134
+ }
1135
+ }
1136
+ }
1137
+
1138
+ override def deriveWrapper[F[_, _], A, B](
1139
+ wrapped: Reflect[F, B],
1140
+ typeId: TypeId[A],
1141
+ binding: Binding[BindingType.Wrapper[A, B], A],
1142
+ doc: Doc,
1143
+ modifiers: Seq[Modifier.Reflect],
1144
+ defaultValue: Option[A],
1145
+ examples: Seq[A]
1146
+ )(implicit F: HasBinding[F], D: DeriveGen.HasInstance[F]): Lazy[Gen[A]] = Lazy {
1147
+ val wrappedGen = D.instance(wrapped.metadata)
1148
+ val wrapperBinding = binding.asInstanceOf[Binding.Wrapper[A, B]]
1149
+
1150
+ new Gen[A] {
1151
+ def generate(random: Random): A =
1152
+ wrapperBinding.wrap(wrappedGen.force.generate(random))
1153
+ }
1154
+ }
1155
+ }
1156
+ ```
1157
+
1158
+ ### Primitive Derivation
1159
+
1160
+ 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:
1161
+
1162
+ ```scala
1163
+ def derivePrimitive[A](
1164
+ primitiveType: PrimitiveType[A],
1165
+ typeId: TypeId[A],
1166
+ binding: Binding[BindingType.Primitive, A],
1167
+ doc: Doc,
1168
+ modifiers: Seq[Modifier.Reflect],
1169
+ defaultValue: Option[A],
1170
+ examples: Seq[A]
1171
+ ): Lazy[Gen[A]] =
1172
+ Lazy {
1173
+ new Gen[A] {
1174
+ def generate(random: Random): A = primitiveType match {
1175
+ case _: PrimitiveType.String => random.alphanumeric.take(random.nextInt(10) + 1).mkString.asInstanceOf[A]
1176
+ case _: PrimitiveType.Char => random.alphanumeric.head.asInstanceOf[A]
1177
+ case _: PrimitiveType.Boolean => random.nextBoolean().asInstanceOf[A]
1178
+ case _: PrimitiveType.Int => random.nextInt(100).asInstanceOf[A]
1179
+ case _: PrimitiveType.Long => random.nextLong().asInstanceOf[A]
1180
+ case _: PrimitiveType.Double => random.nextDouble().asInstanceOf[A]
1181
+ case PrimitiveType.Unit => ().asInstanceOf[A]
1182
+ // For brevity, other primitives default to their zero/empty value
1183
+ // In a real implementation, you would want to handle all primitives and possibly use modifiers for ranges, etc.
1184
+ case _ => defaultValue.getOrElse(null.asInstanceOf[A])
1185
+ }
1186
+ }
1187
+ }
1188
+ ```
1189
+
1190
+ 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.).
1191
+
1192
+ ### Record Derivation
1193
+
1194
+ 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:
1195
+
1196
+ ```scala
1197
+ def deriveRecord[F[_, _], A](
1198
+ fields: IndexedSeq[Term[F, A, ?]],
1199
+ typeId: TypeId[A],
1200
+ binding: Binding[BindingType.Record, A],
1201
+ doc: Doc,
1202
+ modifiers: Seq[Modifier.Reflect],
1203
+ defaultValue: Option[A],
1204
+ examples: Seq[A]
1205
+ )(implicit F: HasBinding[F], D: DeriveGen.HasInstance[F]): Lazy[Gen[A]] =
1206
+ Lazy {
1207
+ // Get Gen instances for each field
1208
+ val fieldGens: IndexedSeq[Lazy[Gen[Any]]] = fields.map { field =>
1209
+ D.instance(field.value.metadata).asInstanceOf[Lazy[Gen[Any]]]
1210
+ }
1211
+
1212
+ // Build Reflect.Record to access registers and constructor
1213
+ val recordFields = fields.asInstanceOf[IndexedSeq[Term[Binding, A, ?]]]
1214
+ val recordBinding = binding.asInstanceOf[Binding.Record[A]]
1215
+ val recordReflect = new Reflect.Record[Binding, A](recordFields, typeId, recordBinding, doc, modifiers)
1216
+
1217
+ new Gen[A] {
1218
+ def generate(random: Random): A = {
1219
+ // Create registers to hold field values
1220
+ val registers = Registers(recordReflect.usedRegisters)
1221
+
1222
+ // Generate each field and store in registers
1223
+ fields.indices.foreach { i =>
1224
+ val value = fieldGens(i).force.generate(random)
1225
+ recordReflect.registers(i).set(registers, RegisterOffset.Zero, value)
1226
+ }
1227
+
1228
+ // Construct the record from registers
1229
+ recordBinding.constructor.construct(registers, RegisterOffset.Zero)
1230
+ }
1231
+ }
1232
+ }
1233
+ ```
1234
+
1235
+ 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`.
1236
+
1237
+ ### Variant Derivation
1238
+
1239
+ The `deriveVariant` method is responsible for deriving a `Gen` instance for variant types, such as sealed traits with case classes:
1240
+
1241
+ ```scala
1242
+ def deriveVariant[F[_, _], A](
1243
+ cases: IndexedSeq[Term[F, A, ?]],
1244
+ typeId: TypeId[A],
1245
+ binding: Binding[BindingType.Variant, A],
1246
+ doc: Doc,
1247
+ modifiers: Seq[Modifier.Reflect],
1248
+ defaultValue: Option[A],
1249
+ examples: Seq[A]
1250
+ )(implicit F: HasBinding[F], D: DeriveGen.HasInstance[F]): Lazy[Gen[A]] = Lazy {
1251
+ // Get Gen instances for all cases
1252
+ val caseGens: IndexedSeq[Lazy[Gen[A]]] = cases.map { c =>
1253
+ D.instance(c.value.metadata).asInstanceOf[Lazy[Gen[A]]]
1254
+ }
1255
+
1256
+ new Gen[A] {
1257
+ def generate(random: Random): A = {
1258
+ // Pick a random case and generate its value
1259
+ val caseIndex = random.nextInt(cases.length)
1260
+ caseGens(caseIndex).force.generate(random)
1261
+ }
1262
+ }
1263
+ }
1264
+ ```
1265
+
1266
+ 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.
1267
+
1268
+ ### Sequence Derivation
1269
+
1270
+ The `deriveSequence` method is responsible for deriving a `Gen` instance for sequence types, such as `List[A]`:
1271
+
1272
+ ```scala
1273
+ def deriveSequence[F[_, _], C[_], A](
1274
+ element: Reflect[F, A],
1275
+ typeId: TypeId[C[A]],
1276
+ binding: Binding[BindingType.Seq[C], C[A]],
1277
+ doc: Doc,
1278
+ modifiers: Seq[Modifier.Reflect],
1279
+ defaultValue: Option[C[A]],
1280
+ examples: Seq[C[A]]
1281
+ )(implicit F: HasBinding[F], D: DeriveGen.HasInstance[F]): Lazy[Gen[C[A]]] = Lazy {
1282
+ val elementGen = D.instance(element.metadata)
1283
+ val seqBinding = binding.asInstanceOf[Binding.Seq[C, A]]
1284
+ val constructor = seqBinding.constructor
1285
+
1286
+ new Gen[C[A]] {
1287
+ def generate(random: Random): C[A] = {
1288
+ val length = random.nextInt(6) // 0 to 5 elements
1289
+ implicit val ct: scala.reflect.ClassTag[A] = scala.reflect.ClassTag.Any.asInstanceOf[scala.reflect.ClassTag[A]]
1290
+
1291
+ if (length == 0) {
1292
+ constructor.empty[A]
1293
+ } else {
1294
+ val builder = constructor.newBuilder[A](length)
1295
+ (0 until length).foreach { _ =>
1296
+ constructor.add(builder, elementGen.force.generate(random))
1297
+ }
1298
+ constructor.result(builder)
1299
+ }
1300
+ }
1301
+ }
1302
+ }
1303
+ ```
1304
+
1305
+ 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.
1306
+
1307
+ ### Map Derivation
1308
+
1309
+ The `deriveMap` method is responsible for deriving a `Gen` instance for map types, such as `Map[K, V]`:
1310
+
1311
+ ```scala
1312
+ def deriveMap[F[_, _], M[_, _], K, V](
1313
+ key: Reflect[F, K],
1314
+ value: Reflect[F, V],
1315
+ typeId: TypeId[M[K, V]],
1316
+ binding: Binding[BindingType.Map[M], M[K, V]],
1317
+ doc: Doc,
1318
+ modifiers: Seq[Modifier.Reflect],
1319
+ defaultValue: Option[M[K, V]],
1320
+ examples: Seq[M[K, V]]
1321
+ )(implicit F: HasBinding[F], D: DeriveGen.HasInstance[F]): Lazy[Gen[M[K, V]]] = Lazy {
1322
+ val keyGen = D.instance(key.metadata)
1323
+ val valueGen = D.instance(value.metadata)
1324
+ val mapBinding = binding.asInstanceOf[Binding.Map[M, K, V]]
1325
+ val constructor = mapBinding.constructor
1326
+
1327
+ new Gen[M[K, V]] {
1328
+ def generate(random: Random): M[K, V] = {
1329
+ val size = random.nextInt(6) // 0 to 5 entries
1330
+
1331
+ if (size == 0) {
1332
+ constructor.emptyObject[K, V]
1333
+ } else {
1334
+ val builder = constructor.newObjectBuilder[K, V](size)
1335
+ (0 until size).foreach { _ =>
1336
+ constructor.addObject(builder, keyGen.force.generate(random), valueGen.force.generate(random))
1337
+ }
1338
+ constructor.resultObject(builder)
1339
+ }
1340
+ }
1341
+ }
1342
+ }
1343
+ ```
1344
+
1345
+ The derivation process for maps is similar to sequences, but it requires handling the generation of random values for both keys and values.
1346
+
1347
+ ### Dynamic Derivation
1348
+
1349
+ 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:
1350
+
1351
+ ```scala
1352
+ def deriveDynamic[F[_, _]](
1353
+ binding: Binding[BindingType.Dynamic, DynamicValue],
1354
+ doc: Doc,
1355
+ modifiers: Seq[Modifier.Reflect],
1356
+ defaultValue: Option[DynamicValue],
1357
+ examples: Seq[DynamicValue]
1358
+ )(implicit F: HasBinding[F], D: DeriveGen.HasInstance[F]): Lazy[Gen[DynamicValue]] = Lazy {
1359
+ new Gen[DynamicValue] {
1360
+ // Helper to generate a random primitive value
1361
+ private def randomPrimitive(random: Random): DynamicValue.Primitive = {
1362
+ val primitiveType = random.nextInt(5)
1363
+ primitiveType match {
1364
+ case 0 => DynamicValue.Primitive(PrimitiveValue.Int(random.nextInt()))
1365
+ case 1 => DynamicValue.Primitive(PrimitiveValue.String(random.alphanumeric.take(10).mkString))
1366
+ case 2 => DynamicValue.Primitive(PrimitiveValue.Boolean(random.nextBoolean()))
1367
+ case 3 => DynamicValue.Primitive(PrimitiveValue.Double(random.nextDouble()))
1368
+ case 4 => DynamicValue.Primitive(PrimitiveValue.Long(random.nextLong()))
1369
+ }
1370
+ }
1371
+
1372
+ def generate(random: Random): DynamicValue = {
1373
+ // Randomly choose what kind of DynamicValue to generate
1374
+ // Weight towards primitives and simpler structures to avoid deep nesting
1375
+ val valueType = random.nextInt(10)
1376
+ valueType match {
1377
+ case 0 | 1 | 2 | 3 | 4 =>
1378
+ // 50% chance: generate a primitive
1379
+ randomPrimitive(random)
1380
+
1381
+ case 5 | 6 =>
1382
+ // 20% chance: generate a record with 1-3 fields
1383
+ val numFields = random.nextInt(3) + 1
1384
+ val fields = (0 until numFields).map { i =>
1385
+ val fieldName = s"field$i"
1386
+ val fieldValue = randomPrimitive(random)
1387
+ (fieldName, fieldValue: DynamicValue)
1388
+ }
1389
+ DynamicValue.Record(Chunk.from(fields))
1390
+
1391
+ case 7 | 8 =>
1392
+ // 20% chance: generate a sequence of 0-3 primitives
1393
+ val numElements = random.nextInt(4)
1394
+ val elements = (0 until numElements).map(_ => randomPrimitive(random): DynamicValue)
1395
+ DynamicValue.Sequence(Chunk.from(elements))
1396
+
1397
+ case 9 =>
1398
+ // 10% chance: generate null
1399
+ DynamicValue.Null
1400
+ }
1401
+ }
1402
+ }
1403
+ }
1404
+ ```
1405
+
1406
+ Please note that the random generation logic in this example is basic and is intended for illustrative purposes only.
1407
+
1408
+ ### Wrapper Derivation
1409
+
1410
+ The `deriveWrapper` method is responsible for deriving a `Gen` instance for wrapper types, such as value classes or opaque types:
1411
+
1412
+ ```scala
1413
+ def deriveWrapper[F[_, _], A, B](
1414
+ wrapped: Reflect[F, B],
1415
+ typeId: TypeId[A],
1416
+ binding: Binding[BindingType.Wrapper[A, B], A],
1417
+ doc: Doc,
1418
+ modifiers: Seq[Modifier.Reflect],
1419
+ defaultValue: Option[A],
1420
+ examples: Seq[A]
1421
+ )(implicit F: HasBinding[F], D: DeriveGen.HasInstance[F]): Lazy[Gen[A]] = Lazy {
1422
+ val wrappedGen = D.instance(wrapped.metadata)
1423
+ val wrapperBinding = binding.asInstanceOf[Binding.Wrapper[A, B]]
1424
+
1425
+ new Gen[A] {
1426
+ def generate(random: Random): A =
1427
+ wrapperBinding.wrap(wrappedGen.force.generate(random))
1428
+ }
1429
+ }
1430
+ ```
1431
+
1432
+ 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.
1433
+
1434
+ ### Example Usages
1435
+
1436
+ 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.
1437
+
1438
+ 1. Example 1: Simple `Person` Record with Two Primitive Fields:
1439
+
1440
+ ```scala
1441
+ case class Person(name: String, age: Int)
1442
+
1443
+ object Person {
1444
+ implicit val schema: Schema[Person] = Schema.derived[Person]
1445
+ implicit val gen: Gen[Person] = schema.derive(DeriveGen)
1446
+ }
1447
+ ```
1448
+
1449
+ Now we can use the derived `Gen[Person]` instance to generate random `Person` values:
1450
+
1451
+ ```scala
1452
+ val random = new Random(42) // Seeded for reproducible output
1453
+ // random: Random = scala.util.Random@1d9748ca
1454
+
1455
+ Person.gen.generate(random)
1456
+ // res14: Person = Person(name = "p", age = -1360544799)
1457
+ Person.gen.generate(random)
1458
+ // res15: Person = Person(name = "C7DgX", age = 392236186)
1459
+ Person.gen.generate(random)
1460
+ // res16: Person = Person(name = "AM6", age = 1184328952)
1461
+ ```
1462
+
1463
+ 2. Simple Shape Variant (Circle, Rectangle)
1464
+
1465
+ ```scala
1466
+ sealed trait Shape
1467
+ case class Circle(radius: Double) extends Shape
1468
+ case class Rectangle(width: Double, height: Double) extends Shape
1469
+
1470
+ object Shape {
1471
+ implicit val schema: Schema[Shape] = Schema.derived[Shape]
1472
+ implicit val gen: Gen[Shape] = schema.derive(DeriveGen)
1473
+ }
1474
+ ```
1475
+
1476
+ To generate random `Shape` values, we can do the following:
1477
+
1478
+ ```scala
1479
+ Shape.gen.generate(random)
1480
+ // res17: Shape = Rectangle(
1481
+ // width = 0.46365357580915334,
1482
+ // height = 0.7829017787900358
1483
+ // )
1484
+ Shape.gen.generate(random)
1485
+ // res18: Shape = Rectangle(
1486
+ // width = 0.15195824856297624,
1487
+ // height = 0.43979982659080874
1488
+ // )
1489
+ Shape.gen.generate(random)
1490
+ // res19: Shape = Rectangle(
1491
+ // width = 0.38656687435934867,
1492
+ // height = 0.17737847790937833
1493
+ // )
1494
+ Shape.gen.generate(random)
1495
+ // res20: Shape = Rectangle(
1496
+ // width = 0.338307935145014,
1497
+ // height = 0.2506613258416336
1498
+ // )
1499
+ ```
1500
+
1501
+ 3. Team with Sequence of Members (List)
1502
+
1503
+ ```scala
1504
+ case class Team(members: List[String])
1505
+
1506
+ object Team {
1507
+ implicit val schema: Schema[Team] = Schema.derived[Team]
1508
+ implicit val gen: Gen[Team] = schema.derive(DeriveGen)
1509
+ }
1510
+ ```
1511
+
1512
+ Let's generate some random `Team` values:
1513
+
1514
+ ```scala
1515
+ Team.gen.generate(random)
1516
+ // res21: Team = Team(List("zZY", "TZlZMZdVjx", "G", "iqf1Pt9", "S1q6qHNj0R"))
1517
+ Team.gen.generate(random)
1518
+ // res22: Team = Team(List("b94sbz0WFC"))
1519
+ Team.gen.generate(random)
1520
+ // res23: Team = Team(List("nwyT"))
1521
+ ```
1522
+
1523
+ 4. Example 4: Recursive Tree
1524
+
1525
+ ```scala
1526
+ case class Tree(value: Int, children: List[Tree])
1527
+
1528
+ object Tree {
1529
+ implicit val schema: Schema[Tree] = Schema.derived[Tree]
1530
+ implicit val gen: Gen[Tree] = schema.derive(DeriveGen)
1531
+ }
1532
+ ```
1533
+
1534
+ 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:
1535
+
1536
+ ```scala
1537
+ Tree.gen.generate(random)
1538
+ // res24: Tree = Tree(value = 1205047495, children = List())
1539
+ ```
1540
+
1541
+ 5. Example 5: DynamicValue Example
1542
+
1543
+ ```scala
1544
+ implicit val dynamicGen: Gen[DynamicValue] = Schema.dynamic.derive(DeriveGen)
1545
+ ```
1546
+
1547
+ Let's generate some random `DynamicValue` instances:
1548
+
1549
+ ```scala
1550
+ dynamicGen.generate(random)
1551
+ // res25: DynamicValue = Primitive(Int(769973518))
1552
+ dynamicGen.generate(random)
1553
+ // res26: DynamicValue = Primitive(Long(8878934151639676041L))
1554
+ dynamicGen.generate(random)
1555
+ // res27: DynamicValue = Sequence(IndexedSeq(Primitive(Int(-1576812231))))
1556
+ ```
1557
+
1558
+ 6. Example 6: Simple Email Wrapper Type
1559
+
1560
+ ```scala
1561
+ case class Email(value: String)
1562
+
1563
+ object Email {
1564
+ implicit val schema: Schema[Email] = Schema[String].transform(
1565
+ Email(_),
1566
+ _.value
1567
+ )
1568
+ implicit val gen: Gen[Email] = schema.derive(DeriveGen)
1569
+ }
1570
+ ```
1571
+
1572
+ The `Email` type is a simple wrapper around `String`. Let's see how it generates random `Email` values:
1573
+
1574
+ ```scala
1575
+ Email.gen.generate(random)
1576
+ // res28: Email = Email("zlLKVaEitt")
1577
+ Email.gen.generate(random)
1578
+ // res29: Email = Email("Sa")
1579
+ ```
1580
+
1581
+ ## Custom Type-class Instances
1582
+
1583
+ 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.
1584
+
1585
+ 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)`:
1586
+
1587
+ ```scala
1588
+ val schema: Schema[A] = ...
1589
+ val deriver: Deriver[TC] = ...
1590
+
1591
+ // Using derive: fully automatic, no customization
1592
+ val tc: TC[A] = schema.derive(deriver)
1593
+
1594
+ // Using deriving: returns a DerivationBuilder for customization
1595
+ val tc: TC[A] = schema.deriving(deriver)
1596
+ .instance(...) // override specific instances
1597
+ .modifier(...) // override specific modifiers
1598
+ .derive // finalize the derivation
1599
+ ```
1600
+
1601
+ The `DerivationBuilder` offers two overloaded `instance` methods for providing custom type class instances:
1602
+
1603
+ ```scala
1604
+ final case class DerivationBuilder[TC[_], A](...) {
1605
+ def instance[B](optic: Optic[A, B], instance: => TC[B]): DerivationBuilder[TC, A]
1606
+ def instance[B](typeId: TypeId[B], instance: => TC[B]): DerivationBuilder[TC, A]
1607
+ }
1608
+ ```
1609
+
1610
+ ### Overriding by Optic
1611
+
1612
+ 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:
1613
+
1614
+ ```scala
1615
+ import zio.blocks.schema._
1616
+ import zio.blocks.typeid.TypeId
1617
+
1618
+ case class Person(name: String, age: Int)
1619
+
1620
+ object Person extends CompanionOptics[Person] {
1621
+ implicit val schema: Schema[Person] = Schema.derived[Person]
1622
+
1623
+ val name: Lens[Person, String] = $(_.name)
1624
+ val age: Lens[Person, Int] = $(_.age)
1625
+ }
1626
+ ```
1627
+
1628
+ Now we can override the `Show[String]` instance specifically for the `name` field of `Person`:
1629
+
1630
+ ```scala
1631
+ val customNameShow: Show[String] = new Show[String] {
1632
+ def show(value: String): String = value.toUpperCase
1633
+ }
1634
+
1635
+ val personShow: Show[Person] = Person.schema
1636
+ .deriving(DeriveShow)
1637
+ .instance(Person.name, customNameShow)
1638
+ .derive
1639
+ ```
1640
+
1641
+ 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:
1642
+
1643
+ ```scala
1644
+ personShow.show(Person("Alice", 30))
1645
+ // res30: String = "Person(name = ALICE, age = 30)"
1646
+ ```
1647
+
1648
+ 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`:
1649
+
1650
+ ```scala
1651
+ case class Company(ceo: Person, industry: String)
1652
+
1653
+ object Company extends CompanionOptics[Company] {
1654
+ implicit val schema: Schema[Company] = Schema.derived[Company]
1655
+
1656
+ val ceo: Lens[Company, Person] = $(_.ceo)
1657
+ val ceoName: Lens[Company, String] = $(_.ceo.name)
1658
+ val industry: Lens[Company, String] = $(_.industry)
1659
+ }
1660
+ ```
1661
+
1662
+ ```scala
1663
+ val companyShow: Show[Company] = Company.schema
1664
+ .deriving(DeriveShow)
1665
+ .instance(Company.ceoName, customNameShow)
1666
+ .derive
1667
+ ```
1668
+
1669
+ 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:
1670
+
1671
+ ```scala
1672
+ companyShow.show(Company(Person("Alice", 30), "tech"))
1673
+ // res31: String = "Company(ceo = Person(name = ALICE, age = 30), industry = \"tech\")"
1674
+ ```
1675
+
1676
+ ### Overriding by TypeId
1677
+
1678
+ 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:
1679
+
1680
+ ```scala
1681
+ val customIntShow: Show[Int] = new Show[Int] {
1682
+ def show(value: Int): String = s"#$value"
1683
+ }
1684
+
1685
+ val personShow: Show[Person] = Person.schema
1686
+ .deriving(DeriveShow)
1687
+ .instance(TypeId.int, customIntShow)
1688
+ .derive
1689
+ ```
1690
+
1691
+ All `Int` fields in the `Person` schema (in this case, just `age`) will use the custom `Show[Int]` instance:
1692
+
1693
+ ```scala
1694
+ personShow.show(Person("Alice", 30))
1695
+ // res32: String = "Person(name = \"Alice\", age = #30)"
1696
+ ```
1697
+
1698
+ ### Resolution Order
1699
+
1700
+ When the derivation engine encounters a schema node, it resolves the type class instance using the following priority order:
1701
+
1702
+ 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.
1703
+ 2. **TypeId-based override** (more general): If no optic-based match is found, it checks for an instance override registered by type ID.
1704
+ 3. **Automatic derivation** (default): If no override is found, the deriver's method (e.g., `derivePrimitive`, `deriveRecord`) is called to automatically derive the instance.
1705
+
1706
+ This means you can set a global override by type and then selectively refine specific fields using optics:
1707
+
1708
+ ```scala
1709
+ val companyShow: Show[Company] = Company.schema
1710
+ .deriving(DeriveShow)
1711
+ .instance(TypeId.string, new Show[String] {
1712
+ def show(value: String): String = s"'$value'"
1713
+ })
1714
+ .instance(Company.ceoName, new Show[String] {
1715
+ def show(value: String): String = value.toUpperCase
1716
+ })
1717
+ .derive
1718
+ ```
1719
+
1720
+ In this example, all `String` fields use single quotes, except for the CEO's name which is shown in uppercase:
1721
+
1722
+ ```scala
1723
+ companyShow.show(Company(Person("Alice", 30), "tech"))
1724
+ // res33: String = "Company(ceo = Person(name = ALICE, age = 30), industry = 'tech')"
1725
+ ```
1726
+
1727
+ ### Chaining Multiple Overrides
1728
+
1729
+ The `instance` method returns a new `DerivationBuilder`, so you can chain multiple overrides fluently:
1730
+
1731
+ ```scala
1732
+ val personShow: Show[Person] = Person.schema
1733
+ .deriving(DeriveShow)
1734
+ .instance(Person.name, new Show[String] {
1735
+ def show(value: String): String = s"<<$value>>"
1736
+ })
1737
+ .instance(Person.age, new Show[Int] {
1738
+ def show(value: Int): String = s"age=$value"
1739
+ })
1740
+ .derive
1741
+ ```
1742
+
1743
+ ```scala
1744
+ personShow.show(Person("Alice", 30))
1745
+ // res34: String = "Person(name = <<Alice>>, age = age=30)"
1746
+ ```
1747
+
1748
+ ## Custom Modifiers
1749
+
1750
+ 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.
1751
+
1752
+ 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:
1753
+
1754
+ - You don't control the schema definition (e.g., it comes from a library)
1755
+ - You need different modifiers for different derivation contexts (e.g., one JSON codec with renamed fields, another without)
1756
+ - You want to keep the schema clean and push format-specific concerns into the derivation layer
1757
+
1758
+ The `DerivationBuilder` offers two overloaded `modifier` methods:
1759
+
1760
+ ```scala
1761
+ final case class DerivationBuilder[TC[_], A](...) {
1762
+ def modifier[B](typeId: TypeId[B], modifier: Modifier.Reflect): DerivationBuilder[TC, A]
1763
+ def modifier[B](optic: Optic[A, B], modifier: Modifier) : DerivationBuilder[TC, A]
1764
+ }
1765
+ ```
1766
+
1767
+ ### Modifier Hierarchy
1768
+
1769
+ ZIO Blocks has two categories of modifiers:
1770
+
1771
+ - **`Modifier.Reflect`**: Type-level modifiers that apply to the schema node itself (e.g., `Modifier.config`).
1772
+ - **`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`).
1773
+
1774
+ Note that `Modifier.config` extends both `Modifier.Term` and `Modifier.Reflect`, so it can be used at both levels.
1775
+
1776
+ ### Adding Modifiers by Optic
1777
+
1778
+ 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:
1779
+
1780
+ ```scala
1781
+ import zio.blocks.schema.json._
1782
+
1783
+ case class User(
1784
+ id: Long,
1785
+ name: String,
1786
+ email: String,
1787
+ internalScore: Double
1788
+ )
1789
+
1790
+ object User extends CompanionOptics[User] {
1791
+ implicit val schema: Schema[User] = Schema.derived[User]
1792
+
1793
+ val id: Lens[User, Long] = $(_.id)
1794
+ val name: Lens[User, String] = $(_.name)
1795
+ val email: Lens[User, String] = $(_.email)
1796
+ val internalScore: Lens[User, Double] = $(_.internalScore)
1797
+ }
1798
+ ```
1799
+
1800
+ Now we can derive a JSON codec with custom modifiers, renaming fields and marking one as transient, without changing the schema itself:
1801
+
1802
+ ```scala
1803
+ val jsonCodec: JsonBinaryCodec[User] = User.schema
1804
+ .deriving(JsonBinaryCodecDeriver)
1805
+ .modifier(User.name, Modifier.rename("full_name"))
1806
+ .modifier(User.email, Modifier.alias("mail"))
1807
+ .modifier(User.internalScore, Modifier.transient())
1808
+ .derive
1809
+ ```
1810
+
1811
+ In this example:
1812
+ - The `name` field will be serialized as `full_name` in JSON.
1813
+ - The `email` field will accept both `email` and `mail` as keys during deserialization.
1814
+ - The `internalScore` field will be excluded from serialization entirely.
1815
+
1816
+ ```scala
1817
+ val user = User(1L, "Alice", "alice@example.com", 95.5)
1818
+ // user: User = User(
1819
+ // id = 1L,
1820
+ // name = "Alice",
1821
+ // email = "alice@example.com",
1822
+ // internalScore = 95.5
1823
+ // )
1824
+ new String(jsonCodec.encode(user), "UTF-8")
1825
+ // res35: String = "{\"id\":1,\"full_name\":\"Alice\",\"email\":\"alice@example.com\"}"
1826
+ ```
1827
+
1828
+ ### Adding Modifiers by TypeId
1829
+
1830
+ 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:
1831
+
1832
+ ```scala
1833
+ val jsonCodec: JsonBinaryCodec[User] = User.schema
1834
+ .deriving(JsonBinaryCodecDeriver)
1835
+ .modifier(TypeId.of[User], Modifier.config("json", "camelCase"))
1836
+ .modifier(User.internalScore, Modifier.transient())
1837
+ .derive
1838
+ ```
1839
+
1840
+ ## Derivation Process In-Depth
1841
+
1842
+ 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.
1843
+
1844
+ ### PHASE 1: Deriving the Schema for the Target Type
1845
+
1846
+ 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:
1847
+
1848
+ For example, assume a case class of `Person(name: String, age: Int)`. The derived schema would look like this:
1849
+
1850
+ ```
1851
+ Schema[Person]
1852
+ └── Reflect.Record[Binding, Person]
1853
+ ├── Term("name", Reflect.Primitive[Binding, String])
1854
+ └── Term("age", Reflect.Primitive[Binding, Int])
1855
+ ```
1856
+
1857
+ Each node of the derived schema tree, carries two pieces of information:
1858
+ - **Type Metadata**: Structural representation of the type (e.g., record, variant, primitive).
1859
+ - **Binding Metadata**: Structural binding information for constructing/deconstructing values of that type.
1860
+
1861
+ 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.
1862
+
1863
+ For example, the following code derives the schema for `Person`:
1864
+
1865
+ ```scala
1866
+ case class Person(name: String, age: Int)
1867
+
1868
+ object Person {
1869
+ implicit val schema: Schema[Person] = Schema.derived[Person]
1870
+ }
1871
+ ```
1872
+
1873
+ ### PHASE 2: Schema Tree Transformation
1874
+
1875
+ 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.
1876
+
1877
+ 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.
1878
+
1879
+ 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.
1880
+
1881
+ The following diagram illustrates this transformation process:
1882
+
1883
+ ```
1884
+ ┌──────────────────────────────┐
1885
+ │ Reflect[Binding,A] │
1886
+ ├──────────────────────────────┤
1887
+ │ STRUCTURAL BINDING ONLY │
1888
+ └──────────────────────────────┘
1889
+
1890
+ │ transform
1891
+
1892
+ ┌──────────────────────────────┐
1893
+ │ Reflect[BindingInstance,A] │
1894
+ ├──────────────────────────────┤
1895
+ │ STRUCTURAL BINDING │
1896
+ │ WITH TYPE-CLASS INSTANCE │
1897
+ └──────────────────────────────┘
1898
+
1899
+ │ extract
1900
+
1901
+ ┌──────────────────────────────┐
1902
+ │ Lazy[TC[A]] │
1903
+ ├──────────────────────────────┤
1904
+ │ TYPE-CLASS INSTANCE │
1905
+ │ (TC[A]) │
1906
+ └──────────────────────────────┘
1907
+ ```
1908
+
1909
+ The `BindingInstance` is a container that bundles together a structural `Binding` and a derived type class instance `TC[A]`:
1910
+
1911
+ ```scala
1912
+ case class BindingInstance[TC[_], T, A](
1913
+ binding: Binding[T, A], // Original runtime binding
1914
+ instance: Lazy[TC[A]] // The derived type-class instance
1915
+ )
1916
+ ```
1917
+
1918
+ For example, the transformation sequence for the `Person` data type would look like this:
1919
+
1920
+ ```scala
1921
+ case class Person(name: String, age: Int)
1922
+
1923
+ object Person {
1924
+ implicit val schema: Schema[Person] = Schema.derived[Person]
1925
+ implicit val show: Show[Person] = schema.derive(DeriveShow)
1926
+ }
1927
+ ```
1928
+
1929
+ - Step 1: Transform Primitive "name" (String)
1930
+ - deriver.derivePrimitive(String) → Lazy[Show[String]]
1931
+ - Creating BindingInstance(Binding.Primitive, Lazy[Show[String]])
1932
+ - Converting reflect node of `String` Schema from `Reflect[Binding, String]` to `Reflect[BindingInstance, String]`
1933
+
1934
+ - Step 2: Transform Primitive "age" (Int)
1935
+ - deriver.derivePrimitive(Int) → Lazy[Show[Int]]
1936
+ - Creating BindingInstance(Binding.Primitive, Lazy[Show[Int]])
1937
+ - Converting reflect node of `Int` Schema from `Reflect[Binding, Int]` to `Reflect[BindingInstance, Int]`
1938
+
1939
+ - Step 3: Transform Record "Person"
1940
+ - deriver.deriveRecord(fields with transformed metadata) → Lazy[Show[Person]]
1941
+ - Creating BindingInstance(Binding.Record, Lazy[Show[Person]])
1942
+ - Converting reflect node of `Person` Schema from `Reflect[Binding, Person]` to `Reflect[BindingInstance, Person]`
1943
+
1944
+ ### PHASE 3: Extracting the Derived Type Class Instance
1945
+
1946
+ 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]`.
1947
+
1948
+ ### Phase 4: Using the Derived Show Instance
1949
+
1950
+ 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:
1951
+
1952
+ ```scala
1953
+ val result = Person.show.show(Person("Alice", 30))
1954
+ // result: String = "Person(name = Alice, age = 30)"
1955
+ ```
1956
+
1957
+ 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.
1958
+
1959
+ 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.