-
-
Notifications
You must be signed in to change notification settings - Fork 67
Derive ShowPretty #490
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Derive ShowPretty #490
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| 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: | ||
| 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) | ||
|
Comment on lines
+23
to
+26
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why is this so different than the rest of derivations? 🤔
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also I guess we would be creating ambiguous implicits with
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh yeah I forgot about |
||
|
|
||
| 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] = | ||
| new Product[A] {} | ||
|
|
||
| given [A](using inst: => K0.CoproductInstances[Or, A]): DerivedShowPretty[A] = | ||
| new Coproduct[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 | ||
| 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: 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: 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 | ||
| case h :: t => lines = s" ${labels(i)} = $h" :: t.init.map(s => " " + s) ::: s" ${t.last}," :: lines | ||
| i -= 1 | ||
|
|
||
| lines = s"$prefix(" :: lines | ||
|
|
||
| lines | ||
|
|
||
| trait Coproduct[A](using inst: K0.CoproductInstances[Or, A]) extends ShowPretty[A]: | ||
| def showLines(a: A): List[String] = | ||
| inst.fold(a)([t] => (st: Or[t], t: t) => st.apply(t)) | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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[Show[A]]): ShowPretty[A] = DerivedShowPretty[A] | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,193 @@ | ||
| package cats | ||
| package derived | ||
|
|
||
| import cats.laws.discipline.SerializableTests | ||
| import scala.compiletime.* | ||
|
|
||
| class ShowPrettySuite extends KittensSuite: | ||
| import ShowPrettySuite.* | ||
| import ShowPrettySuite.given | ||
| import TestDefns.* | ||
|
|
||
| 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")) | ||
| val pretty = """ | ||
| |Foo( | ||
| | i = 42, | ||
| | b = Some(Hello) | ||
| |) | ||
| """.stripMargin.trim | ||
|
|
||
| assertEquals(showPretty(value), pretty) | ||
| } | ||
|
|
||
| test(s"$context.ShowPretty[Outer]") { | ||
| val value = Outer(Inner(3)) | ||
| val pretty = """ | ||
| |Outer( | ||
| | in = Inner( | ||
| | i = 3 | ||
| | ) | ||
| |) | ||
| """.stripMargin.trim | ||
|
|
||
| assertEquals(showPretty(value), 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 | ||
|
|
||
| assertEquals(showPretty(value), pretty) | ||
| } | ||
|
|
||
| test(s"$context.ShowPretty[GenericAdt[Int]]") { | ||
| val value: GenericAdt[Int] = GenericAdtCase(Some(1)) | ||
| val pretty = """ | ||
| |GenericAdtCase( | ||
| | value = Some(1) | ||
| |) | ||
| """.stripMargin.trim | ||
|
|
||
| assertEquals(showPretty(value), 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 | ||
|
|
||
| assertEquals(showPretty(value), 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 | ||
|
|
||
| assertEquals(showPretty(value), 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 = Vector(4, 5, 6), | ||
| | s = 789 | ||
| |) | ||
| """.stripMargin.trim | ||
|
|
||
| assertEquals(showPretty(value), 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 | ||
|
|
||
| assertEquals(showPretty(value), pretty) | ||
| } | ||
|
|
||
| test(s"$context.ShowPretty respects existing instances") { | ||
| val value = Box(Bogus(42)) | ||
| val pretty = """ | ||
| |Box( | ||
| | content = Blah | ||
| |) | ||
| """.stripMargin.trim | ||
|
|
||
| assertEquals(showPretty(value), pretty) | ||
| } | ||
| } | ||
|
|
||
| locally { | ||
| import auto.showPretty.given | ||
| testShowPretty("auto") | ||
| } | ||
|
|
||
| locally { | ||
| import semiInstances.given | ||
| testShowPretty("semiauto") | ||
| } | ||
|
|
||
| object ShowPrettySuite: | ||
| import TestDefns.* | ||
|
|
||
| given 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is really nice 👍