From b6a5b95c3aae318ef72e5c433c865685847b9bac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Fri, 28 Jun 2024 15:21:21 +0200 Subject: [PATCH 1/3] add Opaque util to act Scala 3 Opaque Types --- .../avsystem/commons/opaque/BaseOpaque.scala | 13 +++++ .../avsystem/commons/opaque/Castable.scala | 17 ++++++ .../com/avsystem/commons/opaque/Opaque.scala | 19 +++++++ .../avsystem/commons/opaque/Subopaque.scala | 15 +++++ .../commons/opaque/CastableTest.scala | 43 +++++++++++++++ .../avsystem/commons/opaque/OpaqueTest.scala | 52 ++++++++++++++++++ .../commons/opaque/SubopaqueTest.scala | 55 +++++++++++++++++++ 7 files changed, 214 insertions(+) create mode 100644 core/src/main/scala/com/avsystem/commons/opaque/BaseOpaque.scala create mode 100644 core/src/main/scala/com/avsystem/commons/opaque/Castable.scala create mode 100644 core/src/main/scala/com/avsystem/commons/opaque/Opaque.scala create mode 100644 core/src/main/scala/com/avsystem/commons/opaque/Subopaque.scala create mode 100644 core/src/test/scala/com/avsystem/commons/opaque/CastableTest.scala create mode 100644 core/src/test/scala/com/avsystem/commons/opaque/OpaqueTest.scala create mode 100644 core/src/test/scala/com/avsystem/commons/opaque/SubopaqueTest.scala diff --git a/core/src/main/scala/com/avsystem/commons/opaque/BaseOpaque.scala b/core/src/main/scala/com/avsystem/commons/opaque/BaseOpaque.scala new file mode 100644 index 000000000..e3fda6fe7 --- /dev/null +++ b/core/src/main/scala/com/avsystem/commons/opaque/BaseOpaque.scala @@ -0,0 +1,13 @@ +package com.avsystem.commons +package opaque + +import com.avsystem.commons.opaque.Castable.<:> + +private[opaque] trait BaseOpaque[From] extends Castable.Ops { + trait Tag + type Type + + implicit protected final val castable: From <:> Type = new Castable[From, Type] + + def apply(value: From): Type +} diff --git a/core/src/main/scala/com/avsystem/commons/opaque/Castable.scala b/core/src/main/scala/com/avsystem/commons/opaque/Castable.scala new file mode 100644 index 000000000..43bb97fa2 --- /dev/null +++ b/core/src/main/scala/com/avsystem/commons/opaque/Castable.scala @@ -0,0 +1,17 @@ +package com.avsystem.commons +package opaque + +private[opaque] final class Castable[A, B] { + @inline def apply(a: A): B = a.asInstanceOf[B] +} +private[opaque] object Castable { + type <:>[A, B] = Castable[A, B] + def apply[A, B](implicit ev: A <:> B): Castable[A, B] = ev + + private[opaque] trait Ops { + @inline final def wrap[From, To](value: From)(implicit ev: From <:> To): To = value.asInstanceOf[To] + @inline final def unwrap[From, To](value: To)(implicit ev: From <:> To): From = value.asInstanceOf[From] + @inline final def wrapF[From, To, F[_]](value: F[From])(implicit ev: From <:> To): F[To] = value.asInstanceOf[F[To]] + @inline final def unwrapF[From, To, F[_]](value: F[To])(implicit ev: From <:> To): F[From] = value.asInstanceOf[F[From]] + } +} diff --git a/core/src/main/scala/com/avsystem/commons/opaque/Opaque.scala b/core/src/main/scala/com/avsystem/commons/opaque/Opaque.scala new file mode 100644 index 000000000..f1a8569a7 --- /dev/null +++ b/core/src/main/scala/com/avsystem/commons/opaque/Opaque.scala @@ -0,0 +1,19 @@ +package com.avsystem.commons +package opaque + +import com.avsystem.commons.opaque.Opaque.Hidden + +trait Opaque[From] extends BaseOpaque[From] { + + final type Type = Hidden[From, Tag] +} + +object Opaque { + + type Hidden[From, Tag] + @inline implicit def classTag[From, Tag](implicit base: ClassTag[From]): ClassTag[Hidden[From, Tag]] = ClassTag(base.runtimeClass) + + trait Default[From] extends Opaque[From] { + override final def apply(value: From): Type = wrap(value) + } +} \ No newline at end of file diff --git a/core/src/main/scala/com/avsystem/commons/opaque/Subopaque.scala b/core/src/main/scala/com/avsystem/commons/opaque/Subopaque.scala new file mode 100644 index 000000000..961091981 --- /dev/null +++ b/core/src/main/scala/com/avsystem/commons/opaque/Subopaque.scala @@ -0,0 +1,15 @@ +package com.avsystem.commons +package opaque + +trait Subopaque[From] extends BaseOpaque[From] { + + final type Type = From & Tag + def apply(value: From): Type +} + +object Subopaque { + + trait Default[From] extends Subopaque[From] { + override final def apply(value: From): Type = wrap(value) + } +} diff --git a/core/src/test/scala/com/avsystem/commons/opaque/CastableTest.scala b/core/src/test/scala/com/avsystem/commons/opaque/CastableTest.scala new file mode 100644 index 000000000..2280f6540 --- /dev/null +++ b/core/src/test/scala/com/avsystem/commons/opaque/CastableTest.scala @@ -0,0 +1,43 @@ +package com.avsystem.commons +package opaque + +import org.scalatest.funsuite.AnyFunSuiteLike + +final class CastableTest extends AnyFunSuiteLike { + test("cast only works for compatible types") { + assertCompiles( + //language=scala + """ + |new Castable.Ops { + | implicit val intToLong: Castable[Int, Long] = new Castable[Int, Long] + | + | wrap[Int, Long](42) + | unwrap[Int, Long](42L) + | wrapF[Int, Long, List](List(42)) + | unwrapF[Int, Long, List](List(42L)) + | + | wrap(42) + | unwrap(42L) + | wrapF(List(42)) + | unwrapF(List(42L)) + | } + |""".stripMargin, + ) + assertDoesNotCompile( + //language=scala + """ + |new Castable.Ops { + | wrap[Int, Long](42) + | unwrap[Int, Long](42L) + | wrapF[Int, Long, List](List(42)) + | unwrapF[Int, Long, List](List(42L)) + | + | wrap(42) + | unwrap(42L) + | wrapF(List(42)) + | unwrapF(List(42L)) + | } + |""".stripMargin, + ) + } +} diff --git a/core/src/test/scala/com/avsystem/commons/opaque/OpaqueTest.scala b/core/src/test/scala/com/avsystem/commons/opaque/OpaqueTest.scala new file mode 100644 index 000000000..d716081b6 --- /dev/null +++ b/core/src/test/scala/com/avsystem/commons/opaque/OpaqueTest.scala @@ -0,0 +1,52 @@ +package com.avsystem.commons +package opaque + +import com.avsystem.commons.opaque.OpaqueTest.* +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +final class OpaqueTest extends AnyFlatSpec with Matchers { + + "Opaque" should "create a type with no runtime overhead" in { + PosInt(1) shouldEqual 1 + PosInt(-1) shouldEqual 0 + } + + it should "not be a subtype of its Repr" in { + type Foo = Foo.Type + object Foo extends Opaque.Default[Int] + assertCompiles("Foo(1): Foo") + assertDoesNotCompile("Foo(1): Int") + } + + it should "support user ops" in { + (PosInt(3) -- PosInt(1)) shouldEqual PosInt(2) + } + + it should "work in Arrays" in { + object Foo extends Opaque.Default[Int] + + val foo = Foo(42) + Array(foo).apply(0) shouldEqual foo + } + + "Opaque.Default" should "automatically create an apply method" in { + object PersonId extends Opaque.Default[Int] + PersonId(1) shouldEqual 1 + } +} + +object OpaqueTest { + object PosInt extends Opaque[Int] { + def apply(value: Int): Type = wrap { + if (value < 0) 0 else value + } + + implicit final class Ops(private val me: PosInt.Type) extends AnyVal { + def --(other: PosInt.Type): PosInt.Type = wrap { + val result = unwrap(me) - unwrap(other) + if (result < 0) 0 else result + } + } + } +} \ No newline at end of file diff --git a/core/src/test/scala/com/avsystem/commons/opaque/SubopaqueTest.scala b/core/src/test/scala/com/avsystem/commons/opaque/SubopaqueTest.scala new file mode 100644 index 000000000..f699c80b6 --- /dev/null +++ b/core/src/test/scala/com/avsystem/commons/opaque/SubopaqueTest.scala @@ -0,0 +1,55 @@ +package com.avsystem.commons +package opaque + +import com.avsystem.commons.opaque.Subopaque.* +import com.avsystem.commons.opaque.SubopaqueTest.* +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +final class SubopaqueTest extends AnyFlatSpec with Matchers { + + "SubOpaque" should "create a type with no runtime overhead" in { + PosInt(1) shouldEqual 1 + PosInt(-1) shouldEqual 0 + } + + it should "be a subtype of its Repr" in { + type Foo = Foo.Type + object Foo extends Subopaque.Default[Int] + assertCompiles("Foo(1): Foo") + assertCompiles("Foo(1): Int") + + Foo(1) - Foo(0) shouldEqual Foo(1) + } + + it should "support user ops" in { + (PosInt(3) -- PosInt(1)) shouldEqual PosInt(2) + } + + it should "work in Arrays" in { + object Foo extends Subopaque.Default[Int] + + val foo = Foo(42) + Array(foo).apply(0) shouldEqual foo + } + + "Subopaque.Default" should "automatically create an apply method" in { + object PersonId extends Subopaque.Default[Int] + PersonId(1) shouldEqual 1 + } +} + +object SubopaqueTest { + object PosInt extends Subopaque[Int] { + def apply(value: Int): Type = wrap { + if (value < 0) 0 else value + } + + implicit final class Ops(private val me: PosInt.Type) extends AnyVal { + def --(other: PosInt.Type): PosInt.Type = wrap { + val result = unwrap(me) - unwrap(other) + if (result < 0) 0 else result + } + } + } +} \ No newline at end of file From 392dc3c1f5923c4a1b4430b212206daffa645e1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Kozak?= Date: Tue, 16 Jul 2024 16:59:29 +0200 Subject: [PATCH 2/3] remove unused `apply` method from Castable, add `codec` to `BaseOpaque` --- .../scala/com/avsystem/commons/opaque/BaseOpaque.scala | 7 +++++-- .../main/scala/com/avsystem/commons/opaque/Castable.scala | 4 +--- .../com/avsystem/commons/serialization/CodecTestData.scala | 4 ++++ .../commons/serialization/GenCodecRoundtripTest.scala | 5 +++++ 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/core/src/main/scala/com/avsystem/commons/opaque/BaseOpaque.scala b/core/src/main/scala/com/avsystem/commons/opaque/BaseOpaque.scala index e3fda6fe7..dfc465f30 100644 --- a/core/src/main/scala/com/avsystem/commons/opaque/BaseOpaque.scala +++ b/core/src/main/scala/com/avsystem/commons/opaque/BaseOpaque.scala @@ -2,12 +2,15 @@ package com.avsystem.commons package opaque import com.avsystem.commons.opaque.Castable.<:> +import com.avsystem.commons.serialization.GenCodec private[opaque] trait BaseOpaque[From] extends Castable.Ops { trait Tag type Type - implicit protected final val castable: From <:> Type = new Castable[From, Type] - def apply(value: From): Type + + + implicit protected final val castable: From <:> Type = new Castable[From, Type] + implicit final def codec(implicit fromCodec: GenCodec[From]): GenCodec[Type] = wrapF(fromCodec) } diff --git a/core/src/main/scala/com/avsystem/commons/opaque/Castable.scala b/core/src/main/scala/com/avsystem/commons/opaque/Castable.scala index 43bb97fa2..919dccd2e 100644 --- a/core/src/main/scala/com/avsystem/commons/opaque/Castable.scala +++ b/core/src/main/scala/com/avsystem/commons/opaque/Castable.scala @@ -1,9 +1,7 @@ package com.avsystem.commons package opaque -private[opaque] final class Castable[A, B] { - @inline def apply(a: A): B = a.asInstanceOf[B] -} +private[opaque] final class Castable[A, B] private[opaque] object Castable { type <:>[A, B] = Castable[A, B] def apply[A, B](implicit ev: A <:> B): Castable[A, B] = ev diff --git a/core/src/test/scala/com/avsystem/commons/serialization/CodecTestData.scala b/core/src/test/scala/com/avsystem/commons/serialization/CodecTestData.scala index 7b3c94240..ac34d474a 100644 --- a/core/src/test/scala/com/avsystem/commons/serialization/CodecTestData.scala +++ b/core/src/test/scala/com/avsystem/commons/serialization/CodecTestData.scala @@ -4,6 +4,7 @@ package serialization import com.avsystem.commons.annotation.AnnotationAggregate import com.avsystem.commons.meta.{AutoOptionalParams, MacroInstances} import com.avsystem.commons.misc.{AutoNamedEnum, NamedEnumCompanion, TypedKey} +import com.avsystem.commons.opaque.{Opaque, Subopaque} import scala.annotation.meta.getter @@ -134,6 +135,9 @@ object CodecTestData { @transparent case class StringId(id: String) object StringId extends TransparentWrapperCompanion[String, StringId] + object SomeOpaque extends Opaque.Default[Int] + object SomeSubopaque extends Subopaque.Default[Int] + trait HasSomeStr { @name("some.str") def str: String @generated def someStrLen: Int = str.length diff --git a/core/src/test/scala/com/avsystem/commons/serialization/GenCodecRoundtripTest.scala b/core/src/test/scala/com/avsystem/commons/serialization/GenCodecRoundtripTest.scala index 27ba19dac..8dc26c1d6 100644 --- a/core/src/test/scala/com/avsystem/commons/serialization/GenCodecRoundtripTest.scala +++ b/core/src/test/scala/com/avsystem/commons/serialization/GenCodecRoundtripTest.scala @@ -72,6 +72,11 @@ abstract class GenCodecRoundtripTest extends AbstractCodecTest { testRoundtrip(StringId("lolfuu"), "lolfuu") } + test("opaque and subopaque") { + testRoundtrip(SomeOpaque(42)) + testRoundtrip(SomeSubopaque(42)) + } + test("case class") { testRoundtrip(SomeCaseClass("dafuq", List(1, 2, 3))) } From 31d8063364534b2c781d32885662b326784ebcba Mon Sep 17 00:00:00 2001 From: halotukozak Date: Sat, 27 Jul 2024 19:50:54 +0200 Subject: [PATCH 3/3] add keycodec to BaseOpaque --- .../main/scala/com/avsystem/commons/opaque/BaseOpaque.scala | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/main/scala/com/avsystem/commons/opaque/BaseOpaque.scala b/core/src/main/scala/com/avsystem/commons/opaque/BaseOpaque.scala index dfc465f30..17f96e66c 100644 --- a/core/src/main/scala/com/avsystem/commons/opaque/BaseOpaque.scala +++ b/core/src/main/scala/com/avsystem/commons/opaque/BaseOpaque.scala @@ -2,7 +2,7 @@ package com.avsystem.commons package opaque import com.avsystem.commons.opaque.Castable.<:> -import com.avsystem.commons.serialization.GenCodec +import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec} private[opaque] trait BaseOpaque[From] extends Castable.Ops { trait Tag @@ -12,5 +12,6 @@ private[opaque] trait BaseOpaque[From] extends Castable.Ops { implicit protected final val castable: From <:> Type = new Castable[From, Type] - implicit final def codec(implicit fromCodec: GenCodec[From]): GenCodec[Type] = wrapF(fromCodec) + implicit final def transparentCodec(implicit fromCodec: GenCodec[From]): GenCodec[Type] = wrapF(fromCodec) + implicit final def transparentKeyCodec(implicit fromKeyCodec: GenKeyCodec[From]): GenKeyCodec[Type] = wrapF(fromKeyCodec) }