From 52de4c476bb63ffcb5ab358b8d29c6e6364b1613 Mon Sep 17 00:00:00 2001 From: Tim Spence Date: Mon, 30 May 2022 17:38:49 +0100 Subject: [PATCH 1/4] WIP derive ShowPretty --- .../cats/derived/DerivedShowPretty.scala | 65 ++++++ .../main/scala-3/cats/derived/package.scala | 5 + .../cats/derived/ShowPrettySuite.scala | 198 ++++++++++++++++++ 3 files changed, 268 insertions(+) create mode 100644 core/src/main/scala-3/cats/derived/DerivedShowPretty.scala create mode 100644 core/src/test/scala-3/cats/derived/ShowPrettySuite.scala diff --git a/core/src/main/scala-3/cats/derived/DerivedShowPretty.scala b/core/src/main/scala-3/cats/derived/DerivedShowPretty.scala new file mode 100644 index 00000000..62875c82 --- /dev/null +++ b/core/src/main/scala-3/cats/derived/DerivedShowPretty.scala @@ -0,0 +1,65 @@ +package cats.derived + +import cats.Show +import shapeless3.deriving.{Continue, K0, Labelling} + +import scala.annotation.implicitNotFound +import scala.compiletime.* +import scala.deriving.Mirror + +trait ShowPretty[A] extends Show[A]: + def showLines(a: A): List[String] + def show(a: A): String = showLines(a).mkString(System.lineSeparator) + +object ShowPretty: + inline def apply[A](using A: ShowPretty[A]): A.type = A + +@implicitNotFound("""Could not derive an instance of ShowPretty[A] where A = ${A}. +Make sure that A satisfies one of the following conditions: + * it is a case class where all fields have a Show instance + * it is a sealed trait where all subclasses have a Show instance""") +type DerivedShowPretty[A] = Derived[ShowPretty[A]] +object DerivedShowPretty: + type Or[A] = Derived.Or[ShowPretty[A]] + + inline def apply[A]: ShowPretty[A] = + import DerivedShowPretty.given + summonInline[DerivedShowPretty[A]].instance + + given [A](using inst: K0.ProductInstances[Or, A], labelling: Labelling[A]): DerivedShowPretty[A] = + given K0.ProductInstances[ShowPretty, A] = inst.unify + new Product[ShowPretty, A] {} + + given [A](using inst: => K0.CoproductInstances[Or, A]): DerivedShowPretty[A] = + given K0.CoproductInstances[ShowPretty, A] = inst.unify + new Coproduct[ShowPretty, A] {} + + trait Product[F[x] <: ShowPretty[x], A](using inst: K0.ProductInstances[F, A], labelling: Labelling[A]) + extends ShowPretty[A]: + def showLines(a: A): List[String] = + val prefix = labelling.label + val labels = labelling.elemLabels + val n = labels.size + if n <= 0 then List(s"$prefix()") + else + var lines: List[String] = List(")") + val inner = inst.project(a)(n - 1)([t] => (show: F[t], x: t) => show.showLines(x)) + inner match + case Nil => lines = s" ${labels(n - 1)} = \"\"," :: lines + case h :: t => lines = s" ${labels(n - 1)} = $h" :: t.map(s => " " + s) ::: lines + var i = n - 2 + while i >= 0 do + val inner = inst.project(a)(i)([t] => (show: F[t], x: t) => show.showLines(x)) + inner match + case Nil => lines = s" ${labels(i)} = \"\"," :: lines + case v :: Nil => lines = s" ${labels(i)} = $v," :: lines + case h :: t => lines = s" ${labels(i)} = $h" :: t.drop(1).map(s => " " + s) ::: s" ${t.last}," :: lines + i -= 1 + + lines = s"$prefix(" :: lines + + lines + + trait Coproduct[F[x] <: ShowPretty[x], A](using inst: K0.CoproductInstances[F, A]) extends ShowPretty[A]: + def showLines(a: A): List[String] = + inst.fold(a)([t] => (st: F[t], t: t) => st.showLines(t)) diff --git a/core/src/main/scala-3/cats/derived/package.scala b/core/src/main/scala-3/cats/derived/package.scala index 2bd150fa..2ba6f1a6 100644 --- a/core/src/main/scala-3/cats/derived/package.scala +++ b/core/src/main/scala-3/cats/derived/package.scala @@ -29,6 +29,7 @@ extension (x: MonoidK.type) inline def derived[F[_]]: MonoidK[F] = DerivedMonoid extension (x: Contravariant.type) inline def derived[F[_]]: Contravariant[F] = DerivedContravariant[F] extension (x: Invariant.type) inline def derived[F[_]]: Invariant[F] = DerivedInvariant[F] extension (x: PartialOrder.type) inline def derived[A]: PartialOrder[A] = DerivedPartialOrder[A] +extension (x: ShowPretty.type) inline def derived[A]: ShowPretty[A] = DerivedShowPretty[A] object semiauto: inline def eq[A]: Eq[A] = DerivedEq[A] @@ -54,6 +55,7 @@ object semiauto: inline def contravariant[F[_]]: Contravariant[F] = DerivedContravariant[F] inline def invariant[F[_]]: Invariant[F] = DerivedInvariant[F] inline def partialOrder[A]: PartialOrder[A] = DerivedPartialOrder[A] + inline def showPretty[A]: ShowPretty[A] = DerivedShowPretty[A] object auto: object eq: @@ -124,3 +126,6 @@ object auto: object partialOrder: inline given [A](using NotGiven[PartialOrder[A]]): PartialOrder[A] = DerivedPartialOrder[A] + + object showPretty: + inline given [A](using NotGiven[ShowPretty[A]]): ShowPretty[A] = DerivedShowPretty[A] diff --git a/core/src/test/scala-3/cats/derived/ShowPrettySuite.scala b/core/src/test/scala-3/cats/derived/ShowPrettySuite.scala new file mode 100644 index 00000000..bcd6efed --- /dev/null +++ b/core/src/test/scala-3/cats/derived/ShowPrettySuite.scala @@ -0,0 +1,198 @@ +package cats +package derived + +import cats.laws.discipline.SerializableTests + +class ShowPrettySuite extends KittensSuite: + import ShowPrettySuite.* + import TestDefns.* + + def testShowPretty(context: String)(using + foo: ShowPretty[Foo], + outer: ShowPretty[Outer], + intTree: ShowPretty[IntTree], + genericAdt: ShowPretty[GenericAdt[Int]], + people: ShowPretty[People], + listField: ShowPretty[ListField], + interleaved: ShowPretty[Interleaved[Int]], + tree: ShowPretty[Tree[Int]], + boxBogus: ShowPretty[Box[Bogus]] + ): Unit = { + checkAll(s"$context.ShowPretty is Serializable", SerializableTests.serializable(ShowPretty[IntTree])) + + test(s"$context.ShowPretty[Foo]") { + val value = Foo(42, Option("Hello")) + val pretty = """ + |Foo( + | i = 42, + | b = Some(Hello) + |) + """.stripMargin.trim + + assert(value.show == pretty) + } + + test(s"$context.ShowPretty[Outer]") { + val value = Outer(Inner(3)) + val pretty = """ + |Outer( + | in = Inner( + | i = 3 + | ) + |) + """.stripMargin.trim + + assert(value.show == pretty) + } + + test(s"$context.ShowPretty[IntTree]") { + val value: IntTree = IntNode(IntLeaf(1), IntNode(IntNode(IntLeaf(2), IntLeaf(3)), IntLeaf(4))) + val pretty = """ + |IntNode( + | l = IntLeaf( + | t = 1 + | ), + | r = IntNode( + | l = IntNode( + | l = IntLeaf( + | t = 2 + | ), + | r = IntLeaf( + | t = 3 + | ) + | ), + | r = IntLeaf( + | t = 4 + | ) + | ) + |) + """.stripMargin.trim + + assert(value.show == pretty) + } + + test(s"$context.ShowPretty[GenericAdt[Int]]") { + val value: GenericAdt[Int] = GenericAdtCase(Some(1)) + val pretty = """ + |GenericAdtCase( + | value = Some(1) + |) + """.stripMargin.trim + + assert(value.show == pretty) + } + + test(s"$context.ShowPretty[People]") { + val value = People("Kai", ContactInfo("303-123-4567", Address("123 1st St", "New York", "NY"))) + val pretty = """ + |People( + | name = Kai, + | contactInfo = ContactInfo( + | phoneNumber = 303-123-4567, + | address = 123 1st St New York NY + | ) + |) + """.stripMargin.trim + + assert(value.show == pretty) + } + + test(s"$context.ShowPretty[ListField]") { + val value = ListField("a", List(ListFieldChild(1))) + val pretty = """ + |ListField( + | a = a, + | b = List(ListFieldChild( + | c = 1 + | )) + |) + """.stripMargin.trim + + assert(value.show == pretty) + } + + test(s"$context.ShowPretty[Interleaved[Int]]") { + val value = Interleaved(1, 2, 3, Vector(4, 5, 6), "789") + val pretty = """ + |Interleaved( + | i = 1, + | t = 2, + | l = 3, + | tt = List(4, 5, 6), + | s = 789 + |) + """.stripMargin.trim + + assert(value.show == pretty) + } + + test(s"$context.ShowPretty[Tree[Int]]") { + val value: Tree[Int] = Node(Leaf(1), Node(Node(Leaf(2), Leaf(3)), Leaf(4))) + val pretty = """ + |Node( + | left = Leaf( + | value = 1 + | ), + | right = Node( + | left = Node( + | left = Leaf( + | value = 2 + | ), + | right = Leaf( + | value = 3 + | ) + | ), + | right = Leaf( + | value = 4 + | ) + | ) + |) + """.stripMargin.trim + + assert(value.show == pretty) + } + + test(s"$context.ShowPretty respects existing instances") { + val value = Box(Bogus(42)) + val pretty = """ + |Box( + | content = Blah + |) + """.stripMargin.trim + + assert(value.show == pretty) + } + } + + locally { + import auto.showPretty.given + testShowPretty("auto") + } + + locally { + import semiInstances.given + testShowPretty("semiauto") + } + +object ShowPrettySuite: + import TestDefns.* + + implicit val showAddress: Show[Address] = Show.show { a => + List(a.street, a.city, a.state).mkString(" ") + } + + final case class Bogus(value: Int) + object Bogus: + given Show[Bogus] = Show.show(_ => "Blah") + + object semiInstances: + given ShowPretty[Foo] = semiauto.showPretty + given ShowPretty[Outer] = semiauto.showPretty + given ShowPretty[IntTree] = semiauto.showPretty + given ShowPretty[GenericAdt[Int]] = semiauto.showPretty + given ShowPretty[People] = semiauto.showPretty + given ShowPretty[ListFieldChild] = semiauto.showPretty + given ShowPretty[ListField] = semiauto.showPretty + given ShowPretty[Interleaved[Int]] = semiauto.showPretty + given ShowPretty[Tree[Int]] = semiauto.showPretty + given ShowPretty[Box[Bogus]] = semiauto.showPretty From d1db47e04ed400fdd2767acb2c1f17856d65560c Mon Sep 17 00:00:00 2001 From: Tim Spence Date: Tue, 31 May 2022 11:26:42 +0100 Subject: [PATCH 2/4] Derive ShowPretty --- .../cats/derived/DerivedShowPretty.scala | 6 +++++- .../cats/derived/ShowPrettySuite.scala | 20 +++++++++---------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/core/src/main/scala-3/cats/derived/DerivedShowPretty.scala b/core/src/main/scala-3/cats/derived/DerivedShowPretty.scala index 62875c82..bbdb45bf 100644 --- a/core/src/main/scala-3/cats/derived/DerivedShowPretty.scala +++ b/core/src/main/scala-3/cats/derived/DerivedShowPretty.scala @@ -34,6 +34,10 @@ object DerivedShowPretty: given K0.CoproductInstances[ShowPretty, A] = inst.unify new Coproduct[ShowPretty, A] {} + given [A](using A: Show[A]): ShowPretty[A] with + override def show(a: A) = A.show(a) + def showLines(a: A): List[String] = A.show(a).split(System.lineSeparator).toList + trait Product[F[x] <: ShowPretty[x], A](using inst: K0.ProductInstances[F, A], labelling: Labelling[A]) extends ShowPretty[A]: def showLines(a: A): List[String] = @@ -53,7 +57,7 @@ object DerivedShowPretty: inner match case Nil => lines = s" ${labels(i)} = \"\"," :: lines case v :: Nil => lines = s" ${labels(i)} = $v," :: lines - case h :: t => lines = s" ${labels(i)} = $h" :: t.drop(1).map(s => " " + s) ::: s" ${t.last}," :: lines + case h :: t => lines = s" ${labels(i)} = $h" :: t.init.map(s => " " + s) ::: s" ${t.last}," :: lines i -= 1 lines = s"$prefix(" :: lines diff --git a/core/src/test/scala-3/cats/derived/ShowPrettySuite.scala b/core/src/test/scala-3/cats/derived/ShowPrettySuite.scala index bcd6efed..307f2cc0 100644 --- a/core/src/test/scala-3/cats/derived/ShowPrettySuite.scala +++ b/core/src/test/scala-3/cats/derived/ShowPrettySuite.scala @@ -29,7 +29,7 @@ class ShowPrettySuite extends KittensSuite: |) """.stripMargin.trim - assert(value.show == pretty) + assertEquals(value.show, pretty) } test(s"$context.ShowPretty[Outer]") { @@ -42,7 +42,7 @@ class ShowPrettySuite extends KittensSuite: |) """.stripMargin.trim - assert(value.show == pretty) + assertEquals(value.show, pretty) } test(s"$context.ShowPretty[IntTree]") { @@ -68,7 +68,7 @@ class ShowPrettySuite extends KittensSuite: |) """.stripMargin.trim - assert(value.show == pretty) + assertEquals(value.show, pretty) } test(s"$context.ShowPretty[GenericAdt[Int]]") { @@ -79,7 +79,7 @@ class ShowPrettySuite extends KittensSuite: |) """.stripMargin.trim - assert(value.show == pretty) + assertEquals(value.show, pretty) } test(s"$context.ShowPretty[People]") { @@ -94,7 +94,7 @@ class ShowPrettySuite extends KittensSuite: |) """.stripMargin.trim - assert(value.show == pretty) + assertEquals(value.show, pretty) } test(s"$context.ShowPretty[ListField]") { @@ -108,7 +108,7 @@ class ShowPrettySuite extends KittensSuite: |) """.stripMargin.trim - assert(value.show == pretty) + assertEquals(value.show, pretty) } test(s"$context.ShowPretty[Interleaved[Int]]") { @@ -118,12 +118,12 @@ class ShowPrettySuite extends KittensSuite: | i = 1, | t = 2, | l = 3, - | tt = List(4, 5, 6), + | tt = Vector(4, 5, 6), | s = 789 |) """.stripMargin.trim - assert(value.show == pretty) + assertEquals(value.show, pretty) } test(s"$context.ShowPretty[Tree[Int]]") { @@ -149,7 +149,7 @@ class ShowPrettySuite extends KittensSuite: |) """.stripMargin.trim - assert(value.show == pretty) + assertEquals(value.show, pretty) } test(s"$context.ShowPretty respects existing instances") { @@ -160,7 +160,7 @@ class ShowPrettySuite extends KittensSuite: |) """.stripMargin.trim - assert(value.show == pretty) + assertEquals(value.show, pretty) } } From 742dc7a2147bf24f72b84676d36d451cbff4d268 Mon Sep 17 00:00:00 2001 From: Tim Spence Date: Tue, 31 May 2022 12:38:11 +0100 Subject: [PATCH 3/4] Derive ShowPretty WIP --- .../cats/derived/DerivedShowPretty.scala | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/core/src/main/scala-3/cats/derived/DerivedShowPretty.scala b/core/src/main/scala-3/cats/derived/DerivedShowPretty.scala index bbdb45bf..e93ba93a 100644 --- a/core/src/main/scala-3/cats/derived/DerivedShowPretty.scala +++ b/core/src/main/scala-3/cats/derived/DerivedShowPretty.scala @@ -20,26 +20,28 @@ Make sure that A satisfies one of the following conditions: * it is a sealed trait where all subclasses have a Show instance""") type DerivedShowPretty[A] = Derived[ShowPretty[A]] object DerivedShowPretty: - type Or[A] = Derived.Or[ShowPretty[A]] + opaque type Or[A] = A => List[String] + object Or extends OrInstances: + def apply[A](instance: A => List[String]): Or[A] = instance + extension [A](or: Or[A]) def apply(a: A): List[String] = or(a) + + sealed abstract class OrInstances: + inline given [A]: Or[A] = summonFrom { + case instance: Show[A] => Or((a: A) => instance.show(a).split(System.lineSeparator).toList) + case derived: DerivedShowPretty[A] => Or(derived.instance.showLines(_)) + } inline def apply[A]: ShowPretty[A] = import DerivedShowPretty.given summonInline[DerivedShowPretty[A]].instance given [A](using inst: K0.ProductInstances[Or, A], labelling: Labelling[A]): DerivedShowPretty[A] = - given K0.ProductInstances[ShowPretty, A] = inst.unify - new Product[ShowPretty, A] {} + new Product[A] {} given [A](using inst: => K0.CoproductInstances[Or, A]): DerivedShowPretty[A] = - given K0.CoproductInstances[ShowPretty, A] = inst.unify - new Coproduct[ShowPretty, A] {} - - given [A](using A: Show[A]): ShowPretty[A] with - override def show(a: A) = A.show(a) - def showLines(a: A): List[String] = A.show(a).split(System.lineSeparator).toList + new Coproduct[A] {} - trait Product[F[x] <: ShowPretty[x], A](using inst: K0.ProductInstances[F, A], labelling: Labelling[A]) - extends ShowPretty[A]: + trait Product[A](using inst: K0.ProductInstances[Or, A], labelling: Labelling[A]) extends ShowPretty[A]: def showLines(a: A): List[String] = val prefix = labelling.label val labels = labelling.elemLabels @@ -47,13 +49,13 @@ object DerivedShowPretty: if n <= 0 then List(s"$prefix()") else var lines: List[String] = List(")") - val inner = inst.project(a)(n - 1)([t] => (show: F[t], x: t) => show.showLines(x)) + val inner = inst.project(a)(n - 1)([t] => (show: Or[t], x: t) => show.apply(x)) inner match case Nil => lines = s" ${labels(n - 1)} = \"\"," :: lines case h :: t => lines = s" ${labels(n - 1)} = $h" :: t.map(s => " " + s) ::: lines var i = n - 2 while i >= 0 do - val inner = inst.project(a)(i)([t] => (show: F[t], x: t) => show.showLines(x)) + val inner = inst.project(a)(i)([t] => (show: Or[t], x: t) => show.apply(x)) inner match case Nil => lines = s" ${labels(i)} = \"\"," :: lines case v :: Nil => lines = s" ${labels(i)} = $v," :: lines @@ -64,6 +66,6 @@ object DerivedShowPretty: lines - trait Coproduct[F[x] <: ShowPretty[x], A](using inst: K0.CoproductInstances[F, A]) extends ShowPretty[A]: + trait Coproduct[A](using inst: K0.CoproductInstances[Or, A]) extends ShowPretty[A]: def showLines(a: A): List[String] = - inst.fold(a)([t] => (st: F[t], t: t) => st.showLines(t)) + inst.fold(a)([t] => (st: Or[t], t: t) => st.apply(t)) From eaa47f85fcdf8b803877f666a994a465ebca721a Mon Sep 17 00:00:00 2001 From: Tim Spence Date: Tue, 31 May 2022 14:22:38 +0100 Subject: [PATCH 4/4] Fix show pretty derivations --- .../main/scala-3/cats/derived/package.scala | 2 +- .../cats/derived/ShowPrettySuite.scala | 39 ++++++++----------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/core/src/main/scala-3/cats/derived/package.scala b/core/src/main/scala-3/cats/derived/package.scala index 2ba6f1a6..deebb012 100644 --- a/core/src/main/scala-3/cats/derived/package.scala +++ b/core/src/main/scala-3/cats/derived/package.scala @@ -128,4 +128,4 @@ object auto: inline given [A](using NotGiven[PartialOrder[A]]): PartialOrder[A] = DerivedPartialOrder[A] object showPretty: - inline given [A](using NotGiven[ShowPretty[A]]): ShowPretty[A] = DerivedShowPretty[A] + inline given [A](using NotGiven[Show[A]]): ShowPretty[A] = DerivedShowPretty[A] diff --git a/core/src/test/scala-3/cats/derived/ShowPrettySuite.scala b/core/src/test/scala-3/cats/derived/ShowPrettySuite.scala index 307f2cc0..8d51de3a 100644 --- a/core/src/test/scala-3/cats/derived/ShowPrettySuite.scala +++ b/core/src/test/scala-3/cats/derived/ShowPrettySuite.scala @@ -2,23 +2,18 @@ package cats package derived import cats.laws.discipline.SerializableTests +import scala.compiletime.* class ShowPrettySuite extends KittensSuite: import ShowPrettySuite.* + import ShowPrettySuite.given import TestDefns.* - def testShowPretty(context: String)(using - foo: ShowPretty[Foo], - outer: ShowPretty[Outer], - intTree: ShowPretty[IntTree], - genericAdt: ShowPretty[GenericAdt[Int]], - people: ShowPretty[People], - listField: ShowPretty[ListField], - interleaved: ShowPretty[Interleaved[Int]], - tree: ShowPretty[Tree[Int]], - boxBogus: ShowPretty[Box[Bogus]] - ): Unit = { - checkAll(s"$context.ShowPretty is Serializable", SerializableTests.serializable(ShowPretty[IntTree])) + inline def showPretty[A](value: A): String = + summonInline[ShowPretty[A]].show(value) + + inline def testShowPretty(context: String): Unit = { + checkAll(s"$context.ShowPretty is Serializable", SerializableTests.serializable(summonInline[ShowPretty[IntTree]])) test(s"$context.ShowPretty[Foo]") { val value = Foo(42, Option("Hello")) @@ -29,7 +24,7 @@ class ShowPrettySuite extends KittensSuite: |) """.stripMargin.trim - assertEquals(value.show, pretty) + assertEquals(showPretty(value), pretty) } test(s"$context.ShowPretty[Outer]") { @@ -42,7 +37,7 @@ class ShowPrettySuite extends KittensSuite: |) """.stripMargin.trim - assertEquals(value.show, pretty) + assertEquals(showPretty(value), pretty) } test(s"$context.ShowPretty[IntTree]") { @@ -68,7 +63,7 @@ class ShowPrettySuite extends KittensSuite: |) """.stripMargin.trim - assertEquals(value.show, pretty) + assertEquals(showPretty(value), pretty) } test(s"$context.ShowPretty[GenericAdt[Int]]") { @@ -79,7 +74,7 @@ class ShowPrettySuite extends KittensSuite: |) """.stripMargin.trim - assertEquals(value.show, pretty) + assertEquals(showPretty(value), pretty) } test(s"$context.ShowPretty[People]") { @@ -94,7 +89,7 @@ class ShowPrettySuite extends KittensSuite: |) """.stripMargin.trim - assertEquals(value.show, pretty) + assertEquals(showPretty(value), pretty) } test(s"$context.ShowPretty[ListField]") { @@ -108,7 +103,7 @@ class ShowPrettySuite extends KittensSuite: |) """.stripMargin.trim - assertEquals(value.show, pretty) + assertEquals(showPretty(value), pretty) } test(s"$context.ShowPretty[Interleaved[Int]]") { @@ -123,7 +118,7 @@ class ShowPrettySuite extends KittensSuite: |) """.stripMargin.trim - assertEquals(value.show, pretty) + assertEquals(showPretty(value), pretty) } test(s"$context.ShowPretty[Tree[Int]]") { @@ -149,7 +144,7 @@ class ShowPrettySuite extends KittensSuite: |) """.stripMargin.trim - assertEquals(value.show, pretty) + assertEquals(showPretty(value), pretty) } test(s"$context.ShowPretty respects existing instances") { @@ -160,7 +155,7 @@ class ShowPrettySuite extends KittensSuite: |) """.stripMargin.trim - assertEquals(value.show, pretty) + assertEquals(showPretty(value), pretty) } } @@ -177,7 +172,7 @@ class ShowPrettySuite extends KittensSuite: object ShowPrettySuite: import TestDefns.* - implicit val showAddress: Show[Address] = Show.show { a => + given Show[Address] = Show.show { a => List(a.street, a.city, a.state).mkString(" ") }