Skip to content

Commit 9c08de6

Browse files
authored
Merge pull request #20 from SystemFw/recursive
Recursive
2 parents b33a13e + 589a4b9 commit 9c08de6

File tree

3 files changed

+158
-31
lines changed

3 files changed

+158
-31
lines changed

docs/schema.md

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,11 @@ our schemas.
6363
6464
## Schema caching
6565
66-
With the exception of recursive schemas, which are treated later, it's
67-
best to declare schemas as `val`, to allow `Dynosaur` to cache some
68-
transformations.
66+
It's best to declare schemas as `val`, to allow `Dynosaur` to cache
67+
some transformations.
6968
7069
```scala
7170
val mySchema: Schema[Thing] = ??? // good
72-
73-
lazy val myRecursiveSchema: Schema[RecursiveThing] = ??? // good
74-
7571
def mySchema: Schema[Thing] = ??? // best avoided if possible, no caching
7672
```
7773
@@ -845,7 +841,8 @@ Imagine you're dealing with a recursive type, such as:
845841
case class Department(name: String, subdeps: List[Department] = Nil)
846842
```
847843

848-
we will need to define its schema as a `lazy val`, and give it an explicit type:
844+
We will need to define its schema as a `lazy val`, and give it an
845+
explicit type:
849846

850847
```scala mdoc:compile-only
851848
lazy val wrongDepSchema: Schema[Department] = Schema.record { field =>
@@ -857,11 +854,12 @@ lazy val wrongDepSchema: Schema[Department] = Schema.record { field =>
857854

858855
```
859856

860-
this code will compile fine, **but result in infinite recursion at runtime**.
857+
which will compile fine, **but still result in infinite recursion at
858+
runtime**.
861859
To make it work, we need to wrap the recursive occurrence of the
862860
schema in `Schema.defer`, like so:
863861

864-
```scala mdoc:silent
862+
```scala mdoc:compile-only
865863
lazy val depSchema: Schema[Department] = Schema.record { field =>
866864
(
867865
field("name", _.name),
@@ -871,6 +869,21 @@ lazy val depSchema: Schema[Department] = Schema.record { field =>
871869

872870
```
873871

872+
You can avoid these intricacies by using the `recursive` method
873+
instead, which has a similar structure (and type inference behaviour)
874+
to `Schema.record` and `Schema.oneOf`:
875+
876+
```scala mdoc:silent
877+
val depSchema: Schema[Department] = Schema.recursive { rec =>
878+
Schema.record { field =>
879+
(
880+
field("name", _.name),
881+
field("subdeps", _.subdeps)(rec.asList)
882+
).mapN(Department.apply)
883+
}
884+
}
885+
```
886+
874887
<details>
875888
<summary>Click to show the resulting DynamoValue</summary>
876889

@@ -893,13 +906,8 @@ depSchema.write(departments)
893906
```
894907
</details>
895908

896-
> So to recap, to define a recursive schema:
897-
>
898-
> - Declare it as a `lazy val` with an explicit type signature
899-
> - Pass the recursive schema *explicitly* to the `field`s that need it
900-
> - Wrap recursive arguments to `field` in `Schema.defer`
901-
902-
The same principles apply to more complex recursive structures such as ADTs:
909+
`Schema.recursive` can also be used for more complex recursive
910+
structures such as ADTs:
903911

904912
<details>
905913
<summary>Click to show ADT example</summary>
@@ -909,21 +917,23 @@ sealed trait Text
909917
case class Paragraph(text: String) extends Text
910918
case class Section(title: String, contents: List[Text]) extends Text
911919

912-
lazy val textSchema: Schema[Text] = Schema.oneOf[Text] { alt =>
913-
val paragraph = Schema.record[Paragraph] { field =>
914-
field("text", _.text).map(Paragraph.apply)
915-
}
916-
.tag("paragraph")
920+
val textSchema: Schema[Text] = Schema.recursive { rec =>
921+
Schema.oneOf { alt =>
922+
val paragraph = Schema.record[Paragraph] { field =>
923+
field("text", _.text).map(Paragraph.apply)
924+
}
925+
.tag("paragraph")
917926

918-
val section = Schema.record[Section] { field =>
919-
(
920-
field("title", _.title),
921-
field("contents", _.contents)(Schema.defer(textSchema.asList))
922-
).mapN(Section.apply)
923-
}
924-
.tag("section")
927+
val section = Schema.record[Section] { field =>
928+
(
929+
field("title", _.title),
930+
field("contents", _.contents)(rec.asList)
931+
).mapN(Section.apply)
932+
}
933+
.tag("section")
925934

926-
alt(section) |+| alt(paragraph)
935+
alt(section) |+| alt(paragraph)
936+
}
927937
}
928938

929939
```
@@ -944,6 +954,8 @@ textSchema.write(text)
944954
```
945955
</details>
946956

957+
> In summary, use `Schema.recursive` to define a recursive schema.
958+
947959

948960
## String Set, Number Set and Binary Set
949961

modules/core/src/main/scala/Schema.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,10 @@ object Schema {
160160
id.imap(_.value)(DynamoValue.apply)
161161

162162
def defer[A](schema: => Schema[A]): Schema[A] = Defer(() => schema)
163+
def recursive[A](f: Schema[A] => Schema[A]): Schema[A] = {
164+
lazy val schema: Schema[A] = f(defer(schema))
165+
schema
166+
}
163167

164168
def nullable[A](implicit s: Schema[A]): Schema[Option[A]] = s.nullable
165169

modules/core/src/test/scala/SchemaSuite.scala

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ class SchemaSuite extends ScalaCheckSuite {
374374
)
375375
}
376376

377-
test("recursive products") {
377+
test("recursive products with defer") {
378378
val departments = Department(
379379
"STEM",
380380
List(
@@ -422,6 +422,56 @@ class SchemaSuite extends ScalaCheckSuite {
422422
check(schema, departments, expected)
423423
}
424424

425+
test("recursive products with recursive") {
426+
val departments = Department(
427+
"STEM",
428+
List(
429+
Department("CS"),
430+
Department(
431+
"Maths",
432+
List(
433+
Department("Applied"),
434+
Department("Theoretical")
435+
)
436+
)
437+
)
438+
)
439+
440+
val expected = V.m(
441+
"name" -> V.s("STEM"),
442+
"subdeps" -> V.l(
443+
V.m(
444+
"name" -> V.s("CS"),
445+
"subdeps" -> V.l()
446+
),
447+
V.m(
448+
"name" -> V.s("Maths"),
449+
"subdeps" -> V.l(
450+
V.m(
451+
"name" -> V.s("Applied"),
452+
"subdeps" -> V.l()
453+
),
454+
V.m(
455+
"name" -> V.s("Theoretical"),
456+
"subdeps" -> V.l()
457+
)
458+
)
459+
)
460+
)
461+
)
462+
463+
val schema: Schema[Department] = Schema.recursive { rec =>
464+
Schema.record { field =>
465+
(
466+
field("name", _.name),
467+
field("subdeps", _.subdeps)(rec.asList)
468+
).mapN(Department.apply)
469+
}
470+
}
471+
472+
check(schema, departments, expected)
473+
}
474+
425475
test("products with more than 22 fields") {
426476
val big = Big(
427477
"f",
@@ -786,7 +836,7 @@ class SchemaSuite extends ScalaCheckSuite {
786836
check(schema, successfulUp, expectedSuccessful)
787837
}
788838

789-
test("recursive ADTs") {
839+
test("recursive ADTs with defer") {
790840
val text = Section(
791841
"A",
792842
List(
@@ -845,6 +895,67 @@ class SchemaSuite extends ScalaCheckSuite {
845895
check(schema, text, expected)
846896
}
847897

898+
test("recursive ADTs with recursive") {
899+
val text = Section(
900+
"A",
901+
List(
902+
Paragraph("lorem ipsum"),
903+
Section(
904+
"A.b",
905+
List(Paragraph("dolor sit amet"))
906+
)
907+
)
908+
)
909+
910+
val expected = V.m(
911+
"section" -> V.m(
912+
"title" -> V.s("A"),
913+
"contents" -> V.l(
914+
V.m(
915+
"paragraph" -> V.m(
916+
"text" -> V.s("lorem ipsum")
917+
)
918+
),
919+
V.m(
920+
"section" -> V.m(
921+
"title" -> V.s("A.b"),
922+
"contents" -> V.l(
923+
V.m(
924+
"paragraph" -> V.m(
925+
"text" -> V.s("dolor sit amet")
926+
)
927+
)
928+
)
929+
)
930+
)
931+
)
932+
)
933+
)
934+
935+
val schema: Schema[Text] = Schema.recursive { rec =>
936+
Schema.oneOf { alt =>
937+
val paragraph = Schema
938+
.record[Paragraph] { field =>
939+
field("text", _.text).map(Paragraph.apply)
940+
}
941+
.tag("paragraph")
942+
943+
val section = Schema
944+
.record[Section] { field =>
945+
(
946+
field("title", _.title),
947+
field("contents", _.contents)(rec.asList)
948+
).mapN(Section.apply)
949+
}
950+
.tag("section")
951+
952+
alt(section) |+| alt(paragraph)
953+
}
954+
}
955+
956+
check(schema, text, expected)
957+
}
958+
848959
val compileTimeInferenceSpec = {
849960
val userSchema: Schema[User] = Schema.record { field =>
850961
(

0 commit comments

Comments
 (0)