From 13ed1747117ae60f50ab5b10a5081dcc14601153 Mon Sep 17 00:00:00 2001 From: johnhungerford Date: Fri, 13 Dec 2024 13:03:45 -0500 Subject: [PATCH] Show type class (#914) - `Show[A]` type class to generate string representations of types - Generic derivation for ADTs - Default instance for other instances using `toString` - `Show` instances for `Maybe`, `Result`, and `<` displaying wrappers - String interpolator `k` to use `Show` to construct strings - E.g., `println(k"hello ${Maybe.Present("world")}")` produces `hello Present(world)` instead of `hello world` --------- Co-authored-by: Adam Hearn <22334119+hearnadam@users.noreply.github.com> --- README.md | 96 +++++++++++++ .../shared/src/main/scala/kyo/Console.scala | 8 +- kyo-core/shared/src/main/scala/kyo/Log.scala | 24 ++-- .../src/test/scala/kyo/ConsoleTest.scala | 4 +- .../shared/src/main/scala/kyo/Maybe.scala | 14 +- .../shared/src/main/scala/kyo/Render.scala | 100 ++++++++++++++ .../shared/src/main/scala/kyo/Result.scala | 13 +- .../shared/src/test/scala/kyo/MaybeTest.scala | 14 ++ .../src/test/scala/kyo/RenderTest.scala | 128 ++++++++++++++++++ .../src/test/scala/kyo/ResultTest.scala | 15 +- .../src/main/scala/kyo/kernel/Pending.scala | 7 + .../test/scala/kyo/kernel/PendingTest.scala | 9 ++ 12 files changed, 404 insertions(+), 28 deletions(-) create mode 100644 kyo-data/shared/src/main/scala/kyo/Render.scala create mode 100644 kyo-data/shared/src/test/scala/kyo/RenderTest.scala diff --git a/README.md b/README.md index f0525d08f..e463fa47a 100644 --- a/README.md +++ b/README.md @@ -463,6 +463,46 @@ val b: Result[Throwable, Int] = KyoApp.Unsafe.runAndBlock(2.minutes)(a) ``` +### Displaying Kyo types + +Due to the extensive use of opaque types in Kyo, logging Kyo values can lead to confusion, as the output of `toString` will often leave out type information we are used to seeing in boxed types. For instance, when a pure value is lifted to a pending computation, you will see only that value when you print it. + +```scala +import kyo.* + +val a: Int < Any = 23 +Console.printLine(s"Kyo effect: $a") +// Ouput: Kyo effect: 23 +``` + +This can be jarring to new Kyo users, since we would expect a Kyo computation to be something more than just a pure value. In fact, Kyo's ability to treat pure values as effects is part of what makes it so performant. Nevetheless, the string representations can mislead us about the types of values we log, which can make it harder to interpret our logs. To make things clearer, Kyo provides an `Render` utility to generate clearer string representation of types: + +```scala +import kyo.* + +val a: Int < Any = 23 + +val aStr: Text = Render.asText(a) + +Console.printLine(s"Kyo effect: $aStr") +// Output: Kyo effect: Kyo(23) +``` + +We can still see the pure value (23) in the output, but now we can also see that it is a `Kyo`. This will work similarly for other unboxed types like `Maybe` and `Result` (see below). + +Note that `Render` does not convert to string but to `Text`--an enriched `String` alternative provided and used internally by Kyo. Kyo methods for displaying strings all accept `Text` values (see `Console` and `Log`, below). Converting values using `Render` directly can be cumbersome, however, so Kyo also provides a string interpolator to construct properly formatted `Text`s automatically. To use this interpolater, prefix your interpolated strings with `t` instead of `s`. + +```scala +import kyo.* + +val a: Int < Any = 23 + +Console.printLine(t"Kyo effect: $a, Kyo maybe: ${Maybe(23)}") +// Output: Kyo effect: Kyo(23), Kyo maybe: Present(23) +``` + +We recommend using `txt` as the default string interpolator in Kyo applications for the best developer experience. + ## Core Effects Kyo's core effects act as the essential building blocks that power your application's various functionalities. Unlike other libraries that might require heavy boilerplate or specialized knowledge, Kyo's core effects are designed to be straightforward and flexible. These core effects not only simplify the management of side-effects, dependencies, and several other aspects but also allow for a modular approach to building maintainable systems. @@ -492,6 +532,28 @@ val d: Int < Abort[Exception] = Abort.catching(throw new Exception) ``` +To handle a potentially aborting effect, you can use `Abort.run`. This will produce a `Result`, a high-performance Kyo type equivalent to `Either`: + +```scala +import kyo.* + +// The 'get' method "extracts" the value +// from an 'Either' (right projection) +val a: Int < Abort[String] = + Abort.get(Right(1)) + +// short-circuiting via 'Left' +val b: Int < Abort[String] = + Abort.get(Left("failed!")) + +val aRes: Result[String, Int] < Any = Abort.run(a) +val bRes: Result[String, Int] < Any = Abort.run(b) + +// Note we use a t-string since Result is an unboxed type +println(t"A: ${aRes.eval}, B: ${bRes.eval}") +// Output: A: Success(1), B: Fail(failed!) +``` + > Note that the `Abort` effect has a type parameter and its methods can only be accessed if the type parameter is provided. ### IO: Side Effects @@ -1544,6 +1606,8 @@ val f: Unit < (IO & Abort[IOException]) = Console.let(Console.live)(e) ``` +Note that `Console.printX` methods accept `Text` values. `Text` is a super-type of `String`, however, so you can just pass regular strings. You can also pass `Text` instances generated from the `txt` string interpolator ([see above](#displaying-kyo-types)). + ### Clock: Time Management and Scheduled Tasks The `Clock` effect provides utilities for time-related operations, including getting the current time, creating stopwatches, and managing deadlines. @@ -1728,6 +1792,8 @@ val d: Unit < IO = Log.error("example", new Exception) ``` +Note that like `Console`, `Log` methods accept `Text` values. This means they can also accept regular strings as well as outputs of `txt`-interpolation ([see above](#displaying-kyo-types)). + ### Stat: Observability `Stat` is a pluggable implementation that provides counters, histograms, gauges, and tracing. It uses Java's [service loading](https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html) to locate exporters. @@ -2716,6 +2782,21 @@ val result: String = case Absent => "No value" ``` +`Maybe`'s high performance is due to the fact that it is unboxed. Accordingly, we recommend using t-string interpolation when logging `Maybe`s: + +```scala +import kyo.* + +val maybe: Maybe[Maybe[Int]] = Maybe(Maybe(42)) +val maybeNot: Maybe[Maybe[Int]] = Maybe(Maybe.Absent) + +println(s"s-string nested maybes: $maybe, $maybeNot") +// Output: s-string nested maybes: 42, Absent + +println(t"t-string nested maybes: $maybe, $maybeNot") +// Output: t-string nested maybes: Present(Present(42)), Present(Absent) +``` + ### Duration: Time Representation `Duration` provides a convenient and efficient way to represent and manipulate time durations. It offers a wide range of operations and conversions, making it easy to work with time intervals in various units. @@ -2825,6 +2906,21 @@ val q: Try[Int] = a.toTry Under the hood, `Result` is defined as an opaque type that is a supertype of `Success[T]` and `Failure[T]`. Success[T] represents a successful result and is encoded as either the value itself (`T`) or a special SuccessFailure[`T`] case class. The `SuccessFailure[T]` case class is used to handle the rare case where a `Failure[T]` needs to be wrapped in a `Success[T]`. On the other hand, a failed `Result` is always represented by a `Failure[T]` case class, which contains the exception that caused the failure. This means that creating a `Failure[T]` does incur an allocation cost. Additionally, some methods on `Result`, such as `fold`, `map`, and `flatMap`, may allocate in certain cases due to the need to catch and handle exceptions. +Since `Result.Success` is unboxed, we recommend using t-string interpolation when logging `Result`s: + +```scala +import kyo.* + +val success: Result[String, Result[String, Int]] = Result.success(Result.success(42)) +val failure: Result[String, Result[String, Int]] = Result.success(Result.fail("failure!")) + +println(s"s-string nested results: $success, $failure") +// Output: s-string nested results: 42, Fail(failure!) + +println(t"t-string nested results: $success, $failure") +// Output: t-string nested results: Success(Success(42)), Success(Fail(failure!)) +``` + ### TypeMap: Type-Safe Heterogeneous Maps `TypeMap` provides a type-safe heterogeneous map implementation, allowing you to store and retrieve values of different types using their types as keys. This is particularly useful for managing multiple types of data in a single structure with type safety. diff --git a/kyo-core/shared/src/main/scala/kyo/Console.scala b/kyo-core/shared/src/main/scala/kyo/Console.scala index c6fba7ecb..36b8cb6c5 100644 --- a/kyo-core/shared/src/main/scala/kyo/Console.scala +++ b/kyo-core/shared/src/main/scala/kyo/Console.scala @@ -19,28 +19,28 @@ final case class Console(unsafe: Console.Unsafe): * @param s * The string to print. */ - def print(s: String)(using Frame): Unit < (IO & Abort[IOException]) = IO.Unsafe(Abort.get(unsafe.print(s))) + def print(s: Text)(using Frame): Unit < (IO & Abort[IOException]) = IO.Unsafe(Abort.get(unsafe.print(s.show))) /** Prints a string to the console's error stream without a newline. * * @param s * The string to print to the error stream. */ - def printErr(s: String)(using Frame): Unit < (IO & Abort[IOException]) = IO.Unsafe(Abort.get(unsafe.printErr(s))) + def printErr(s: Text)(using Frame): Unit < (IO & Abort[IOException]) = IO.Unsafe(Abort.get(unsafe.printErr(s.show))) /** Prints a string to the console followed by a newline. * * @param s * The string to print. */ - def println(s: String)(using Frame): Unit < (IO & Abort[IOException]) = IO.Unsafe(Abort.get(unsafe.printLine(s))) + def println(s: Text)(using Frame): Unit < (IO & Abort[IOException]) = IO.Unsafe(Abort.get(unsafe.printLine(s.show))) /** Prints a string to the console's error stream followed by a newline. * * @param s * The string to print to the error stream. */ - def printLineErr(s: String)(using Frame): Unit < (IO & Abort[IOException]) = IO.Unsafe(Abort.get(unsafe.printLineErr(s))) + def printLineErr(s: Text)(using Frame): Unit < (IO & Abort[IOException]) = IO.Unsafe(Abort.get(unsafe.printLineErr(s.show))) /** Flushes the console output streams. * diff --git a/kyo-core/shared/src/main/scala/kyo/Log.scala b/kyo-core/shared/src/main/scala/kyo/Log.scala index ed7c620a4..e00de0c64 100644 --- a/kyo-core/shared/src/main/scala/kyo/Log.scala +++ b/kyo-core/shared/src/main/scala/kyo/Log.scala @@ -3,17 +3,19 @@ package kyo import kyo.internal.LogPlatformSpecific final case class Log(unsafe: Log.Unsafe): - def level: Log.Level = unsafe.level - inline def trace(inline msg: => String)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.trace(msg)) - inline def trace(inline msg: => String, inline t: => Throwable)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.trace(msg, t)) - inline def debug(inline msg: => String)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.debug(msg)) - inline def debug(inline msg: => String, inline t: => Throwable)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.debug(msg, t)) - inline def info(inline msg: => String)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.info(msg)) - inline def info(inline msg: => String, inline t: => Throwable)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.info(msg, t)) - inline def warn(inline msg: => String)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.warn(msg)) - inline def warn(inline msg: => String, t: => Throwable)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.warn(msg, t)) - inline def error(inline msg: => String)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.error(msg)) - inline def error(inline msg: => String, t: => Throwable)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.error(msg, t)) + def level: Log.Level = unsafe.level + inline def trace(inline msg: => Text)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.trace(msg.show)) + inline def trace(inline msg: => Text, inline t: => Throwable)(using inline frame: Frame): Unit < IO = + IO.Unsafe(unsafe.trace(msg.show, t)) + inline def debug(inline msg: => Text)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.debug(msg.show)) + inline def debug(inline msg: => Text, inline t: => Throwable)(using inline frame: Frame): Unit < IO = + IO.Unsafe(unsafe.debug(msg.show, t)) + inline def info(inline msg: => Text)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.info(msg.show)) + inline def info(inline msg: => Text, inline t: => Throwable)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.info(msg.show, t)) + inline def warn(inline msg: => Text)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.warn(msg.show)) + inline def warn(inline msg: => Text, t: => Throwable)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.warn(msg.show, t)) + inline def error(inline msg: => Text)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.error(msg.show)) + inline def error(inline msg: => Text, t: => Throwable)(using inline frame: Frame): Unit < IO = IO.Unsafe(unsafe.error(msg.show, t)) end Log /** Logging utility object for Kyo applications. */ diff --git a/kyo-core/shared/src/test/scala/kyo/ConsoleTest.scala b/kyo-core/shared/src/test/scala/kyo/ConsoleTest.scala index e9c418b1b..bb1b9b159 100644 --- a/kyo-core/shared/src/test/scala/kyo/ConsoleTest.scala +++ b/kyo-core/shared/src/test/scala/kyo/ConsoleTest.scala @@ -6,14 +6,14 @@ class ConsoleTest extends Test: val obj = Obj("a") val pprintObj = pprint.apply(obj).toString - "readln" in run { + "readLine" in run { Console.withIn(List("readln")) { Console.readLine.map { result => assert(result == "readln") } } } - "print" in run { + "print string" in run { Console.withOut(Console.print("print")).map { (out, _) => assert(out.stdOut == "print") } diff --git a/kyo-data/shared/src/main/scala/kyo/Maybe.scala b/kyo-data/shared/src/main/scala/kyo/Maybe.scala index 183816d28..173fd10b1 100644 --- a/kyo-data/shared/src/main/scala/kyo/Maybe.scala +++ b/kyo-data/shared/src/main/scala/kyo/Maybe.scala @@ -19,6 +19,14 @@ object Maybe: inline given [A: Flat]: Flat[Maybe[A]] = Flat.unsafe.bypass given [A]: Conversion[Maybe[A], IterableOnce[A]] = _.iterator + given [A, MaybeA <: Maybe[A]](using ra: Render[A]): Render[MaybeA] with + given CanEqual[Absent, MaybeA] = CanEqual.derived + def asText(value: MaybeA): String = (value: Maybe[A]) match + case Present(a) => s"Present(${ra.asText(a)})" + case Absent => "Absent" + case _ => throw IllegalStateException() + end given + /** Creates a Maybe instance from a value. * * @param v @@ -71,9 +79,7 @@ object Maybe: /** Represents a defined value in a Maybe. */ opaque type Present[+A] = A | PresentAbsent - object Present: - /** Creates a Present instance. * * @param v @@ -411,9 +417,7 @@ object Maybe: inline def toResult[E](inline ifEmpty: => Result[E, A]): Result[E, A] = if isEmpty then ifEmpty else Result.success(get) - def show: String = - if isEmpty then "Absent" - else s"Present(${get})" + def show(using r: Render[Maybe[A]]): String = r.asString(self) end extension diff --git a/kyo-data/shared/src/main/scala/kyo/Render.scala b/kyo-data/shared/src/main/scala/kyo/Render.scala new file mode 100644 index 000000000..87611af51 --- /dev/null +++ b/kyo-data/shared/src/main/scala/kyo/Render.scala @@ -0,0 +1,100 @@ +package kyo + +import kyo.Schedule.done +import scala.language.implicitConversions + +/** Provides Text representation of a type. Needed for customizing how to display opaque types as alternative to toString + */ +abstract class Render[A]: + def asText(value: A): Text + final def asString(value: A): String = asText(value).show + +sealed trait LowPriorityRenders: + given [A]: Render[A] with + def asText(value: A): Text = value.toString + +object Render extends LowPriorityRenders: + inline def apply[A](using r: Render[A]): Render[A] = r + + def asText[A](value: A)(using r: Render[A]): Text = r.asText(value) + + import scala.compiletime.* + + private inline def sumRender[A, M <: scala.deriving.Mirror.ProductOf[A]](label: String, mir: M): Render[A] = + val shows = summonAll[Tuple.Map[mir.MirroredElemTypes, Render]] + new Render[A]: + def asText(value: A): String = + val builder = java.lang.StringBuilder() + builder.append(label) + builder.append("(") + val valIter = value.asInstanceOf[Product].productIterator + val showIter: Iterator[Render[Any]] = shows.productIterator.asInstanceOf + if valIter.hasNext then + builder.append(showIter.next().asText(valIter.next())) + () + while valIter.hasNext do + builder.append(",") + builder.append(showIter.next().asText(valIter.next())) + () + end while + builder.append(")") + builder.toString() + end asText + end new + end sumRender + + inline given [A](using mir: scala.deriving.Mirror.Of[A]): Render[A] = inline mir match + case sumMir: scala.deriving.Mirror.SumOf[?] => + val shows = summonAll[Tuple.Map[sumMir.MirroredElemTypes, Render]] + new Render[A]: + def asText(value: A): Text = + val caseIndex = sumMir.ordinal(value) + val showInstance: Render[Any] = shows.productElement(caseIndex).asInstanceOf + showInstance.asText(value) + end asText + end new + case singMir: scala.deriving.Mirror.Singleton => + val label: String = constValue[singMir.MirroredLabel] + new Render[A]: + def asText(value: A): Text = label + case prodMir: scala.deriving.Mirror.ProductOf[?] => inline erasedValue[A] match + case _: Tuple => + inline erasedValue[prodMir.MirroredElemTypes] match + case _: EmptyTuple => + new Render[A]: + def asText(value: A): Text = "()" + case _ => + sumRender[A, prodMir.type]("", prodMir) + case _ => + val label: String = constValue[prodMir.MirroredLabel] + inline erasedValue[prodMir.MirroredElemTypes] match + case _: EmptyTuple => + new Render[A]: + def asText(value: A): Text = label + "()" + case _ => + sumRender[A, prodMir.type](label, prodMir) + end match + +end Render + +sealed trait Rendered: + private[kyo] def textValue: Text + +object Rendered: + given [A](using r: Render[A]): Conversion[A, Rendered] with + def apply(a: A): Rendered = + new Rendered: + private[kyo] def textValue: Text = r.asText(a) + end given +end Rendered + +extension (sc: StringContext) + def t(args: Rendered*): Text = + StringContext.checkLengths(args, sc.parts) + val pi = sc.parts.iterator + val ai = args.iterator + var text: Text = pi.next() + while ai.hasNext do + text = text + ai.next.textValue + text = text + StringContext.processEscapes(pi.next()) + text diff --git a/kyo-data/shared/src/main/scala/kyo/Result.scala b/kyo-data/shared/src/main/scala/kyo/Result.scala index b9f7bbfe4..de54ce4ac 100644 --- a/kyo-data/shared/src/main/scala/kyo/Result.scala +++ b/kyo-data/shared/src/main/scala/kyo/Result.scala @@ -23,6 +23,13 @@ object Result: inline given [E, A: Flat]: Flat[Result[E, A]] = Flat.unsafe.bypass inline given [E, A]: CanEqual[Result[E, A], Panic] = CanEqual.derived + given [E, A, ResultEA <: Result[E, A]](using re: Render[E], ra: Render[A]): Render[ResultEA] with + def asText(value: ResultEA): String = value match + case Success(a) => s"Success(${ra.asText(a.asInstanceOf[A])})" + case f: Fail[?] => s"Fail(${re.asText(f.error.asInstanceOf[E])})" + case other => other.toString() + end given + /** Creates a Result from an expression that might throw an exception. * * @param expr @@ -632,11 +639,7 @@ object Result: * @return * A string describing the Result's state and value */ - def show: String = - self match - case Panic(ex) => s"Panic($ex)" - case Fail(ex) => s"Fail($ex)" - case v => s"Success($v)" + def show(using r: Render[Result[E, A]]): String = r.asString(self) end extension diff --git a/kyo-data/shared/src/test/scala/kyo/MaybeTest.scala b/kyo-data/shared/src/test/scala/kyo/MaybeTest.scala index 60a467997..3e4d35d4e 100644 --- a/kyo-data/shared/src/test/scala/kyo/MaybeTest.scala +++ b/kyo-data/shared/src/test/scala/kyo/MaybeTest.scala @@ -2,6 +2,7 @@ package kyo import kyo.Maybe.* import kyo.Maybe.internal.PresentAbsent +import scala.languageFeature.implicitConversions class MaybeTest extends Test: @@ -532,15 +533,28 @@ class MaybeTest extends Test: "show" - { "should return 'Absent' for Absent" in { assert(Absent.show == "Absent") + assert(t"$Absent".show == "Absent") } "should return 'Present(value)' for Present" in { assert(Present(1).show == "Present(1)") + summon[Conversion[Present[Int], Rendered]] + val somat: Rendered = Present(1) + assert(t"${Present(1): Present[Int]}".show == "Present(1)") assert(Present("hello").show == "Present(hello)") + assert(t"${Present("hello")}".show == "Present(hello)") } "should handle nested Present values" in { assert(Present(Absent).show == "Present(Absent)") + assert(t"${Present(Absent)}".show == "Present(Absent)") + } + + "should return Present(Present(value)) for nested Present" in { + val p: Present[Present[Int]] = Present(Present(1)) + val r: Render[Present[Present[Int]]] = Render.apply + assert(r.asText(p).show == "Present(Present(1))") + assert(t"$p".show == "Present(Present(1))") } } diff --git a/kyo-data/shared/src/test/scala/kyo/RenderTest.scala b/kyo-data/shared/src/test/scala/kyo/RenderTest.scala new file mode 100644 index 000000000..8f95ce7a2 --- /dev/null +++ b/kyo-data/shared/src/test/scala/kyo/RenderTest.scala @@ -0,0 +1,128 @@ +package kyo +import scala.language.implicitConversions + +class RenderTest extends Test: + type Wr[A] = Wr.Type[A] + object Wr: + opaque type Type[A] = A | Null + def apply[A](a: A | Null): Type[A] = a + + given [A](using ra: Render[A]): Render[Wr[A]] with + given CanEqual[A | Null, Null] = CanEqual.derived + def asText(value: Wr[A]): String = if value == null then "Nope" else s"Yep(${ra.asText(value.asInstanceOf[A])})" + end given + end Wr + + case class ShowCase(value: Wr[Int]) + + sealed trait ShowSealed + object ShowSealed: + case object Obj extends ShowSealed + case class Nested(value: Wr[Int]) extends ShowSealed + case class DoubleNested(showCase: ShowCase) extends ShowSealed + end ShowSealed + + enum ShowADT: + case Obj + case Nested(value: Wr[Int]) + case DoubleNested(showCase: ShowCase) + end ShowADT + + enum ShowNestedADT: + case InnerSealed(value: ShowSealed) + case InnerADT(value: ShowADT) + + "derivation" - { + "should derive for a case class correctly" in { + assert(Render[ShowCase].asText(ShowCase(Wr(23))).show == "ShowCase(Yep(23))") + assert(Render.asText(ShowCase(Wr(23))).show == "ShowCase(Yep(23))") + assert(Render[ShowCase].asText(ShowCase(Wr(null))).show == "ShowCase(Nope)") + assert(Render.asText(ShowCase(Wr(null))).show == "ShowCase(Nope)") + } + + "should derive show for sealed hierarchy correctly" in { + assert(Render[ShowSealed.Obj.type].asText(ShowSealed.Obj).show == "Obj") + assert(Render.asText(ShowSealed.Obj).show == "Obj") + assert(Render[ShowSealed].asText(ShowSealed.Obj).show == "Obj") + assert(Render.asText(ShowSealed.Obj).show == "Obj") + val wr: Wr[Int] = Wr(23) + assert(Render[ShowSealed.Nested].asText(ShowSealed.Nested(wr)).show == "Nested(Yep(23))") + assert(Render.asText(ShowSealed.Nested(wr)).show == "Nested(Yep(23))") + assert(Render[ShowSealed].asText(ShowSealed.Nested(wr)).show == "Nested(Yep(23))") + assert(Render.asText(ShowSealed.Nested(wr)).show == "Nested(Yep(23))") + assert(Render[ShowSealed.DoubleNested].asText(ShowSealed.DoubleNested(ShowCase(wr))).show == "DoubleNested(ShowCase(Yep(23)))") + assert(Render.asText(ShowSealed.DoubleNested(ShowCase(wr))).show == "DoubleNested(ShowCase(Yep(23)))") + assert(Render[ShowSealed].asText(ShowSealed.DoubleNested(ShowCase(wr))).show == "DoubleNested(ShowCase(Yep(23)))") + assert(Render.asText(ShowSealed.DoubleNested(ShowCase(wr))).show == "DoubleNested(ShowCase(Yep(23)))") + } + + "should derive show for enum correctly" in { + assert(Render[ShowADT.Obj.type].asText(ShowADT.Obj).show == "Obj") + assert(Render.asText(ShowADT.Obj).show == "Obj") + assert(Render[ShowADT].asText(ShowADT.Obj).show == "Obj") + assert(Render.asText(ShowADT.Obj).show == "Obj") + val wr: Wr[Int] = Wr(23) + assert(Render[ShowADT.Nested].asText(ShowADT.Nested(wr)).show == "Nested(Yep(23))") + assert(Render.asText(ShowADT.Nested(wr)).show == "Nested(Yep(23))") + assert(Render[ShowADT].asText(ShowADT.Nested(wr)).show == "Nested(Yep(23))") + assert(Render.asText(ShowADT.Nested(wr)).show == "Nested(Yep(23))") + assert(Render[ShowADT.DoubleNested].asText(ShowADT.DoubleNested(ShowCase(wr))).show == "DoubleNested(ShowCase(Yep(23)))") + assert(Render.asText(ShowADT.DoubleNested(ShowCase(wr))).show == "DoubleNested(ShowCase(Yep(23)))") + assert(Render[ShowADT].asText(ShowADT.DoubleNested(ShowCase(wr))).show == "DoubleNested(ShowCase(Yep(23)))") + assert(Render.asText(ShowADT.DoubleNested(ShowCase(wr))).show == "DoubleNested(ShowCase(Yep(23)))") + } + + "should derive show for highly nested type" in { + assert(Render[ShowNestedADT].asText( + ShowNestedADT.InnerADT(ShowADT.DoubleNested(ShowCase(Wr(23)))) + ).show == "InnerADT(DoubleNested(ShowCase(Yep(23))))") + assert(Render.asText( + ShowNestedADT.InnerADT(ShowADT.DoubleNested(ShowCase(Wr(23)))) + ).show == "InnerADT(DoubleNested(ShowCase(Yep(23))))") + assert(Render[ShowNestedADT].asText( + ShowNestedADT.InnerSealed(ShowSealed.DoubleNested(ShowCase(Wr(23)))) + ).show == "InnerSealed(DoubleNested(ShowCase(Yep(23))))") + assert(Render.asText( + ShowNestedADT.InnerSealed(ShowSealed.DoubleNested(ShowCase(Wr(23)))) + ).show == "InnerSealed(DoubleNested(ShowCase(Yep(23))))") + } + + "should derive tuple correctly" in { + assert(Render[EmptyTuple].asText(EmptyTuple).show == "EmptyTuple") + assert(Render.asText(EmptyTuple).show == "EmptyTuple") + assert(Render[Tuple1[Wr[String]]].asText(Tuple1(Wr("hello"))).show == "(Yep(hello))") + assert(Render.asText(Tuple1(Wr("hello"))).show == "(Yep(hello))") + assert(Render[(Int, Wr[String])].asText((23, Wr("hello"))).show == "(23,Yep(hello))") + assert(Render.asText((23, Wr("hello"))).show == "(23,Yep(hello))") + assert(Render[(Int, Wr[String], Wr[Nothing])].asText((23, Wr("hello"), Wr(null))).show == "(23,Yep(hello),Nope)") + assert(Render.asText((23, Wr("hello"), Wr(null))).show == "(23,Yep(hello),Nope)") + } + + "should support custom show" in { + given Render[ShowCase] with + def asText(value: ShowCase): String = + "My Custom ShowCase: " + Render.asText(value.value) + + assert(Render[ShowCase].asText(ShowCase(Wr(23))).show == "My Custom ShowCase: Yep(23)") + assert(Render.asText(ShowCase(Wr(23))).show == "My Custom ShowCase: Yep(23)") + assert(Render[ShowCase].asText(ShowCase(Wr(null))).show == "My Custom ShowCase: Nope") + assert(Render.asText(ShowCase(Wr(null))).show == "My Custom ShowCase: Nope") + } + } + + "interpolation" - { + "should interpolate" in { + assert(t"${23}".show == "23") + assert(t"prefix ${Wr(23)} suffix".show == "prefix Yep(23) suffix") + assert(t"prefix ${Wr(null)} suffix".show == "prefix Nope suffix") + assert(t"prefix ${ShowADT.Obj} suffix".show == "prefix Obj suffix") + assert(t"prefix ${ShowADT.Nested(Wr(null))} suffix".show == "prefix Nested(Nope) suffix") + assert(t"prefix ${ShowADT.Nested(Wr(23))} suffix".show == "prefix Nested(Yep(23)) suffix") + } + + "should handle empty string" in { + assert(t"".show == "") + } + } + +end RenderTest diff --git a/kyo-data/shared/src/test/scala/kyo/ResultTest.scala b/kyo-data/shared/src/test/scala/kyo/ResultTest.scala index afcada7fe..431df415b 100644 --- a/kyo-data/shared/src/test/scala/kyo/ResultTest.scala +++ b/kyo-data/shared/src/test/scala/kyo/ResultTest.scala @@ -999,8 +999,21 @@ class ResultTest extends Test: } "nested Success" in { + val nested = Result.success(Result.success(Result.success(23))) + assert(nested.show == "Success(Success(Success(23)))") + assert(t"$nested".show == "Success(Success(Success(23)))") + val widened: Result[Nothing, Result[Nothing, Result[Nothing, Int]]] = nested + assert(widened.show == "Success(Success(Success(23)))") + assert(t"$widened".show == "Success(Success(Success(23)))") + } + + "nested Success with failure" in { val nested = Result.success(Result.success(Result.fail("error"))) - assert(nested.show == "Success(Success(Success(Fail(error))))") + assert(nested.show == "Success(Success(Fail(error)))") + assert(t"$nested".show == "Success(Success(Fail(error)))") + val widened: Result[Nothing, Result[Nothing, Result[String, Nothing]]] = nested + assert(widened.show == "Success(Success(Fail(error)))") + assert(t"$widened".show == "Success(Success(Fail(error)))") } } diff --git a/kyo-prelude/shared/src/main/scala/kyo/kernel/Pending.scala b/kyo-prelude/shared/src/main/scala/kyo/kernel/Pending.scala index 507a2d3ed..b3cf4645c 100644 --- a/kyo-prelude/shared/src/main/scala/kyo/kernel/Pending.scala +++ b/kyo-prelude/shared/src/main/scala/kyo/kernel/Pending.scala @@ -284,4 +284,11 @@ object `<`: using inline flat: WeakFlat[B] ): (A1, A2, A3, A4, A5, A6) => B < Any = (a1, a2, a3, a4, a5, a6) => f(a1, a2, a3, a4, a5, a6) + + given [A, S, APendingS <: A < S](using ra: Render[A]): Render[APendingS] with + def asText(value: APendingS): Text = value match + case sus: Kyo[?, ?] => sus.toString + case a: A @unchecked => s"Kyo(${ra.asText(a)})" + end given + end `<` diff --git a/kyo-prelude/shared/src/test/scala/kyo/kernel/PendingTest.scala b/kyo-prelude/shared/src/test/scala/kyo/kernel/PendingTest.scala index 102f17b16..40e7d106d 100644 --- a/kyo-prelude/shared/src/test/scala/kyo/kernel/PendingTest.scala +++ b/kyo-prelude/shared/src/test/scala/kyo/kernel/PendingTest.scala @@ -269,4 +269,13 @@ class PendingTest extends Test: assert(test(nest((): Unit < Any)).eval == ()) } + "show" - { + "should display pure vals wrapped with inner types displayed using show" in { + val i: Result[String, Int] < Any = Result.success(23) + val r: Render[Result[String, Int] < Any] = Render.apply + assert(r.asText(i).show == "Kyo(Success(23))") + assert(t"$i".show == "Kyo(Success(23))") + } + } + end PendingTest