Skip to content
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

Opaque util to act Scala 3 Opaque Types #592

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
17 changes: 17 additions & 0 deletions core/src/main/scala/com/avsystem/commons/opaque/BaseOpaque.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.avsystem.commons
package opaque

import com.avsystem.commons.opaque.Castable.<:>
import com.avsystem.commons.serialization.{GenCodec, GenKeyCodec}

private[opaque] trait BaseOpaque[From] extends Castable.Ops {
trait Tag
type Type

def apply(value: From): Type


implicit protected final val castable: From <:> Type = new Castable[From, Type]
implicit final def transparentCodec(implicit fromCodec: GenCodec[From]): GenCodec[Type] = wrapF(fromCodec)
implicit final def transparentKeyCodec(implicit fromKeyCodec: GenKeyCodec[From]): GenKeyCodec[Type] = wrapF(fromKeyCodec)
}
15 changes: 15 additions & 0 deletions core/src/main/scala/com/avsystem/commons/opaque/Castable.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.avsystem.commons
package opaque

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

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]]
}
}
19 changes: 19 additions & 0 deletions core/src/main/scala/com/avsystem/commons/opaque/Opaque.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}
15 changes: 15 additions & 0 deletions core/src/main/scala/com/avsystem/commons/opaque/Subopaque.scala
Original file line number Diff line number Diff line change
@@ -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)
}
}
43 changes: 43 additions & 0 deletions core/src/test/scala/com/avsystem/commons/opaque/CastableTest.scala
Original file line number Diff line number Diff line change
@@ -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,
)
}
}
52 changes: 52 additions & 0 deletions core/src/test/scala/com/avsystem/commons/opaque/OpaqueTest.scala
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
}
Expand Down
Loading