Skip to content

Commit

Permalink
add Opaque util to act Scala 3 Opaque Types
Browse files Browse the repository at this point in the history
  • Loading branch information
halotukozak committed Jun 28, 2024
1 parent 5131c93 commit b6a5b95
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 0 deletions.
13 changes: 13 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,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
}
17 changes: 17 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,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]]
}
}
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
}
}
}
}

0 comments on commit b6a5b95

Please sign in to comment.