Skip to content

Commit 522083f

Browse files
authored
Derive ShowPretty (typelevel#490)
* WIP derive ShowPretty * Derive ShowPretty * Derive ShowPretty WIP * Fix show pretty derivations
1 parent f043dda commit 522083f

File tree

3 files changed

+269
-0
lines changed

3 files changed

+269
-0
lines changed
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package cats.derived
2+
3+
import cats.Show
4+
import shapeless3.deriving.{Continue, K0, Labelling}
5+
6+
import scala.annotation.implicitNotFound
7+
import scala.compiletime.*
8+
import scala.deriving.Mirror
9+
10+
trait ShowPretty[A] extends Show[A]:
11+
def showLines(a: A): List[String]
12+
def show(a: A): String = showLines(a).mkString(System.lineSeparator)
13+
14+
object ShowPretty:
15+
inline def apply[A](using A: ShowPretty[A]): A.type = A
16+
17+
@implicitNotFound("""Could not derive an instance of ShowPretty[A] where A = ${A}.
18+
Make sure that A satisfies one of the following conditions:
19+
* it is a case class where all fields have a Show instance
20+
* it is a sealed trait where all subclasses have a Show instance""")
21+
type DerivedShowPretty[A] = Derived[ShowPretty[A]]
22+
object DerivedShowPretty:
23+
opaque type Or[A] = A => List[String]
24+
object Or extends OrInstances:
25+
def apply[A](instance: A => List[String]): Or[A] = instance
26+
extension [A](or: Or[A]) def apply(a: A): List[String] = or(a)
27+
28+
sealed abstract class OrInstances:
29+
inline given [A]: Or[A] = summonFrom {
30+
case instance: Show[A] => Or((a: A) => instance.show(a).split(System.lineSeparator).toList)
31+
case derived: DerivedShowPretty[A] => Or(derived.instance.showLines(_))
32+
}
33+
34+
inline def apply[A]: ShowPretty[A] =
35+
import DerivedShowPretty.given
36+
summonInline[DerivedShowPretty[A]].instance
37+
38+
given [A](using inst: K0.ProductInstances[Or, A], labelling: Labelling[A]): DerivedShowPretty[A] =
39+
new Product[A] {}
40+
41+
given [A](using inst: => K0.CoproductInstances[Or, A]): DerivedShowPretty[A] =
42+
new Coproduct[A] {}
43+
44+
trait Product[A](using inst: K0.ProductInstances[Or, A], labelling: Labelling[A]) extends ShowPretty[A]:
45+
def showLines(a: A): List[String] =
46+
val prefix = labelling.label
47+
val labels = labelling.elemLabels
48+
val n = labels.size
49+
if n <= 0 then List(s"$prefix()")
50+
else
51+
var lines: List[String] = List(")")
52+
val inner = inst.project(a)(n - 1)([t] => (show: Or[t], x: t) => show.apply(x))
53+
inner match
54+
case Nil => lines = s" ${labels(n - 1)} = \"\"," :: lines
55+
case h :: t => lines = s" ${labels(n - 1)} = $h" :: t.map(s => " " + s) ::: lines
56+
var i = n - 2
57+
while i >= 0 do
58+
val inner = inst.project(a)(i)([t] => (show: Or[t], x: t) => show.apply(x))
59+
inner match
60+
case Nil => lines = s" ${labels(i)} = \"\"," :: lines
61+
case v :: Nil => lines = s" ${labels(i)} = $v," :: lines
62+
case h :: t => lines = s" ${labels(i)} = $h" :: t.init.map(s => " " + s) ::: s" ${t.last}," :: lines
63+
i -= 1
64+
65+
lines = s"$prefix(" :: lines
66+
67+
lines
68+
69+
trait Coproduct[A](using inst: K0.CoproductInstances[Or, A]) extends ShowPretty[A]:
70+
def showLines(a: A): List[String] =
71+
inst.fold(a)([t] => (st: Or[t], t: t) => st.apply(t))

core/src/main/scala-3/cats/derived/package.scala

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ extension (x: MonoidK.type) inline def derived[F[_]]: MonoidK[F] = DerivedMonoid
2929
extension (x: Contravariant.type) inline def derived[F[_]]: Contravariant[F] = DerivedContravariant[F]
3030
extension (x: Invariant.type) inline def derived[F[_]]: Invariant[F] = DerivedInvariant[F]
3131
extension (x: PartialOrder.type) inline def derived[A]: PartialOrder[A] = DerivedPartialOrder[A]
32+
extension (x: ShowPretty.type) inline def derived[A]: ShowPretty[A] = DerivedShowPretty[A]
3233

3334
object semiauto:
3435
inline def eq[A]: Eq[A] = DerivedEq[A]
@@ -54,6 +55,7 @@ object semiauto:
5455
inline def contravariant[F[_]]: Contravariant[F] = DerivedContravariant[F]
5556
inline def invariant[F[_]]: Invariant[F] = DerivedInvariant[F]
5657
inline def partialOrder[A]: PartialOrder[A] = DerivedPartialOrder[A]
58+
inline def showPretty[A]: ShowPretty[A] = DerivedShowPretty[A]
5759

5860
object auto:
5961
object eq:
@@ -124,3 +126,6 @@ object auto:
124126

125127
object partialOrder:
126128
inline given [A](using NotGiven[PartialOrder[A]]): PartialOrder[A] = DerivedPartialOrder[A]
129+
130+
object showPretty:
131+
inline given [A](using NotGiven[Show[A]]): ShowPretty[A] = DerivedShowPretty[A]
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package cats
2+
package derived
3+
4+
import cats.laws.discipline.SerializableTests
5+
import scala.compiletime.*
6+
7+
class ShowPrettySuite extends KittensSuite:
8+
import ShowPrettySuite.*
9+
import ShowPrettySuite.given
10+
import TestDefns.*
11+
12+
inline def showPretty[A](value: A): String =
13+
summonInline[ShowPretty[A]].show(value)
14+
15+
inline def testShowPretty(context: String): Unit = {
16+
checkAll(s"$context.ShowPretty is Serializable", SerializableTests.serializable(summonInline[ShowPretty[IntTree]]))
17+
18+
test(s"$context.ShowPretty[Foo]") {
19+
val value = Foo(42, Option("Hello"))
20+
val pretty = """
21+
|Foo(
22+
| i = 42,
23+
| b = Some(Hello)
24+
|)
25+
""".stripMargin.trim
26+
27+
assertEquals(showPretty(value), pretty)
28+
}
29+
30+
test(s"$context.ShowPretty[Outer]") {
31+
val value = Outer(Inner(3))
32+
val pretty = """
33+
|Outer(
34+
| in = Inner(
35+
| i = 3
36+
| )
37+
|)
38+
""".stripMargin.trim
39+
40+
assertEquals(showPretty(value), pretty)
41+
}
42+
43+
test(s"$context.ShowPretty[IntTree]") {
44+
val value: IntTree = IntNode(IntLeaf(1), IntNode(IntNode(IntLeaf(2), IntLeaf(3)), IntLeaf(4)))
45+
val pretty = """
46+
|IntNode(
47+
| l = IntLeaf(
48+
| t = 1
49+
| ),
50+
| r = IntNode(
51+
| l = IntNode(
52+
| l = IntLeaf(
53+
| t = 2
54+
| ),
55+
| r = IntLeaf(
56+
| t = 3
57+
| )
58+
| ),
59+
| r = IntLeaf(
60+
| t = 4
61+
| )
62+
| )
63+
|)
64+
""".stripMargin.trim
65+
66+
assertEquals(showPretty(value), pretty)
67+
}
68+
69+
test(s"$context.ShowPretty[GenericAdt[Int]]") {
70+
val value: GenericAdt[Int] = GenericAdtCase(Some(1))
71+
val pretty = """
72+
|GenericAdtCase(
73+
| value = Some(1)
74+
|)
75+
""".stripMargin.trim
76+
77+
assertEquals(showPretty(value), pretty)
78+
}
79+
80+
test(s"$context.ShowPretty[People]") {
81+
val value = People("Kai", ContactInfo("303-123-4567", Address("123 1st St", "New York", "NY")))
82+
val pretty = """
83+
|People(
84+
| name = Kai,
85+
| contactInfo = ContactInfo(
86+
| phoneNumber = 303-123-4567,
87+
| address = 123 1st St New York NY
88+
| )
89+
|)
90+
""".stripMargin.trim
91+
92+
assertEquals(showPretty(value), pretty)
93+
}
94+
95+
test(s"$context.ShowPretty[ListField]") {
96+
val value = ListField("a", List(ListFieldChild(1)))
97+
val pretty = """
98+
|ListField(
99+
| a = a,
100+
| b = List(ListFieldChild(
101+
| c = 1
102+
| ))
103+
|)
104+
""".stripMargin.trim
105+
106+
assertEquals(showPretty(value), pretty)
107+
}
108+
109+
test(s"$context.ShowPretty[Interleaved[Int]]") {
110+
val value = Interleaved(1, 2, 3, Vector(4, 5, 6), "789")
111+
val pretty = """
112+
|Interleaved(
113+
| i = 1,
114+
| t = 2,
115+
| l = 3,
116+
| tt = Vector(4, 5, 6),
117+
| s = 789
118+
|)
119+
""".stripMargin.trim
120+
121+
assertEquals(showPretty(value), pretty)
122+
}
123+
124+
test(s"$context.ShowPretty[Tree[Int]]") {
125+
val value: Tree[Int] = Node(Leaf(1), Node(Node(Leaf(2), Leaf(3)), Leaf(4)))
126+
val pretty = """
127+
|Node(
128+
| left = Leaf(
129+
| value = 1
130+
| ),
131+
| right = Node(
132+
| left = Node(
133+
| left = Leaf(
134+
| value = 2
135+
| ),
136+
| right = Leaf(
137+
| value = 3
138+
| )
139+
| ),
140+
| right = Leaf(
141+
| value = 4
142+
| )
143+
| )
144+
|)
145+
""".stripMargin.trim
146+
147+
assertEquals(showPretty(value), pretty)
148+
}
149+
150+
test(s"$context.ShowPretty respects existing instances") {
151+
val value = Box(Bogus(42))
152+
val pretty = """
153+
|Box(
154+
| content = Blah
155+
|)
156+
""".stripMargin.trim
157+
158+
assertEquals(showPretty(value), pretty)
159+
}
160+
}
161+
162+
locally {
163+
import auto.showPretty.given
164+
testShowPretty("auto")
165+
}
166+
167+
locally {
168+
import semiInstances.given
169+
testShowPretty("semiauto")
170+
}
171+
172+
object ShowPrettySuite:
173+
import TestDefns.*
174+
175+
given Show[Address] = Show.show { a =>
176+
List(a.street, a.city, a.state).mkString(" ")
177+
}
178+
179+
final case class Bogus(value: Int)
180+
object Bogus:
181+
given Show[Bogus] = Show.show(_ => "Blah")
182+
183+
object semiInstances:
184+
given ShowPretty[Foo] = semiauto.showPretty
185+
given ShowPretty[Outer] = semiauto.showPretty
186+
given ShowPretty[IntTree] = semiauto.showPretty
187+
given ShowPretty[GenericAdt[Int]] = semiauto.showPretty
188+
given ShowPretty[People] = semiauto.showPretty
189+
given ShowPretty[ListFieldChild] = semiauto.showPretty
190+
given ShowPretty[ListField] = semiauto.showPretty
191+
given ShowPretty[Interleaved[Int]] = semiauto.showPretty
192+
given ShowPretty[Tree[Int]] = semiauto.showPretty
193+
given ShowPretty[Box[Bogus]] = semiauto.showPretty

0 commit comments

Comments
 (0)