Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions core/src/main/scala-3/cats/derived/DerivedShowPretty.scala
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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really nice 👍


@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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this so different than the rest of derivations? 🤔
I thought we would either derive ShowPretty directly or use the regular scheme.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ShowPretty is weird because the expectation seems to be that you don't define instances yourself. It's driven off Show OrElse DerivedShowPrettty. This is why we have NotGiven[Show] above as well - otherwise the auto tests fail because they don't honour existing Show instances

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also I guess we would be creating ambiguous implicits with auto otherwise - ShowPretty <: Show so we shouldn't create an instance of ShowPretty if there is already a Show instance for a given type

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh yeah I forgot about Show OrElse DerivedShowPretty 👍


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))
5 changes: 5 additions & 0 deletions core/src/main/scala-3/cats/derived/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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:
Expand Down Expand Up @@ -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]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
inline given [A](using NotGiven[Show[A]]): ShowPretty[A] = DerivedShowPretty[A]
inline given [A](using NotGiven[ShowPretty[A]]): ShowPretty[A] = DerivedShowPretty[A]

193 changes: 193 additions & 0 deletions core/src/test/scala-3/cats/derived/ShowPrettySuite.scala
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