@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.
- package/index.md +49 -29
- package/package.json +1 -1
- package/path-interpolator.md +24 -23
- package/reference/codec.md +384 -0
- package/reference/docs.md +1 -1
- package/reference/dynamic-optic.md +392 -0
- package/reference/formats.md +68 -12
- package/reference/json-schema.md +13 -10
- package/reference/lazy.md +361 -0
- package/reference/modifier.md +340 -0
- package/reference/syntax.md +11 -11
- package/reference/type-class-derivation.md +1959 -0
- package/scope.md +230 -232
- package/sidebars.js +6 -1
|
@@ -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.
|