Skip to content

Commit b6a5b95

Browse files
committed
add Opaque util to act Scala 3 Opaque Types
1 parent 5131c93 commit b6a5b95

File tree

7 files changed

+214
-0
lines changed

7 files changed

+214
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.avsystem.commons
2+
package opaque
3+
4+
import com.avsystem.commons.opaque.Castable.<:>
5+
6+
private[opaque] trait BaseOpaque[From] extends Castable.Ops {
7+
trait Tag
8+
type Type
9+
10+
implicit protected final val castable: From <:> Type = new Castable[From, Type]
11+
12+
def apply(value: From): Type
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.avsystem.commons
2+
package opaque
3+
4+
private[opaque] final class Castable[A, B] {
5+
@inline def apply(a: A): B = a.asInstanceOf[B]
6+
}
7+
private[opaque] object Castable {
8+
type <:>[A, B] = Castable[A, B]
9+
def apply[A, B](implicit ev: A <:> B): Castable[A, B] = ev
10+
11+
private[opaque] trait Ops {
12+
@inline final def wrap[From, To](value: From)(implicit ev: From <:> To): To = value.asInstanceOf[To]
13+
@inline final def unwrap[From, To](value: To)(implicit ev: From <:> To): From = value.asInstanceOf[From]
14+
@inline final def wrapF[From, To, F[_]](value: F[From])(implicit ev: From <:> To): F[To] = value.asInstanceOf[F[To]]
15+
@inline final def unwrapF[From, To, F[_]](value: F[To])(implicit ev: From <:> To): F[From] = value.asInstanceOf[F[From]]
16+
}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.avsystem.commons
2+
package opaque
3+
4+
import com.avsystem.commons.opaque.Opaque.Hidden
5+
6+
trait Opaque[From] extends BaseOpaque[From] {
7+
8+
final type Type = Hidden[From, Tag]
9+
}
10+
11+
object Opaque {
12+
13+
type Hidden[From, Tag]
14+
@inline implicit def classTag[From, Tag](implicit base: ClassTag[From]): ClassTag[Hidden[From, Tag]] = ClassTag(base.runtimeClass)
15+
16+
trait Default[From] extends Opaque[From] {
17+
override final def apply(value: From): Type = wrap(value)
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.avsystem.commons
2+
package opaque
3+
4+
trait Subopaque[From] extends BaseOpaque[From] {
5+
6+
final type Type = From & Tag
7+
def apply(value: From): Type
8+
}
9+
10+
object Subopaque {
11+
12+
trait Default[From] extends Subopaque[From] {
13+
override final def apply(value: From): Type = wrap(value)
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.avsystem.commons
2+
package opaque
3+
4+
import org.scalatest.funsuite.AnyFunSuiteLike
5+
6+
final class CastableTest extends AnyFunSuiteLike {
7+
test("cast only works for compatible types") {
8+
assertCompiles(
9+
//language=scala
10+
"""
11+
|new Castable.Ops {
12+
| implicit val intToLong: Castable[Int, Long] = new Castable[Int, Long]
13+
|
14+
| wrap[Int, Long](42)
15+
| unwrap[Int, Long](42L)
16+
| wrapF[Int, Long, List](List(42))
17+
| unwrapF[Int, Long, List](List(42L))
18+
|
19+
| wrap(42)
20+
| unwrap(42L)
21+
| wrapF(List(42))
22+
| unwrapF(List(42L))
23+
| }
24+
|""".stripMargin,
25+
)
26+
assertDoesNotCompile(
27+
//language=scala
28+
"""
29+
|new Castable.Ops {
30+
| wrap[Int, Long](42)
31+
| unwrap[Int, Long](42L)
32+
| wrapF[Int, Long, List](List(42))
33+
| unwrapF[Int, Long, List](List(42L))
34+
|
35+
| wrap(42)
36+
| unwrap(42L)
37+
| wrapF(List(42))
38+
| unwrapF(List(42L))
39+
| }
40+
|""".stripMargin,
41+
)
42+
}
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.avsystem.commons
2+
package opaque
3+
4+
import com.avsystem.commons.opaque.OpaqueTest.*
5+
import org.scalatest.flatspec.AnyFlatSpec
6+
import org.scalatest.matchers.should.Matchers
7+
8+
final class OpaqueTest extends AnyFlatSpec with Matchers {
9+
10+
"Opaque" should "create a type with no runtime overhead" in {
11+
PosInt(1) shouldEqual 1
12+
PosInt(-1) shouldEqual 0
13+
}
14+
15+
it should "not be a subtype of its Repr" in {
16+
type Foo = Foo.Type
17+
object Foo extends Opaque.Default[Int]
18+
assertCompiles("Foo(1): Foo")
19+
assertDoesNotCompile("Foo(1): Int")
20+
}
21+
22+
it should "support user ops" in {
23+
(PosInt(3) -- PosInt(1)) shouldEqual PosInt(2)
24+
}
25+
26+
it should "work in Arrays" in {
27+
object Foo extends Opaque.Default[Int]
28+
29+
val foo = Foo(42)
30+
Array(foo).apply(0) shouldEqual foo
31+
}
32+
33+
"Opaque.Default" should "automatically create an apply method" in {
34+
object PersonId extends Opaque.Default[Int]
35+
PersonId(1) shouldEqual 1
36+
}
37+
}
38+
39+
object OpaqueTest {
40+
object PosInt extends Opaque[Int] {
41+
def apply(value: Int): Type = wrap {
42+
if (value < 0) 0 else value
43+
}
44+
45+
implicit final class Ops(private val me: PosInt.Type) extends AnyVal {
46+
def --(other: PosInt.Type): PosInt.Type = wrap {
47+
val result = unwrap(me) - unwrap(other)
48+
if (result < 0) 0 else result
49+
}
50+
}
51+
}
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package com.avsystem.commons
2+
package opaque
3+
4+
import com.avsystem.commons.opaque.Subopaque.*
5+
import com.avsystem.commons.opaque.SubopaqueTest.*
6+
import org.scalatest.flatspec.AnyFlatSpec
7+
import org.scalatest.matchers.should.Matchers
8+
9+
final class SubopaqueTest extends AnyFlatSpec with Matchers {
10+
11+
"SubOpaque" should "create a type with no runtime overhead" in {
12+
PosInt(1) shouldEqual 1
13+
PosInt(-1) shouldEqual 0
14+
}
15+
16+
it should "be a subtype of its Repr" in {
17+
type Foo = Foo.Type
18+
object Foo extends Subopaque.Default[Int]
19+
assertCompiles("Foo(1): Foo")
20+
assertCompiles("Foo(1): Int")
21+
22+
Foo(1) - Foo(0) shouldEqual Foo(1)
23+
}
24+
25+
it should "support user ops" in {
26+
(PosInt(3) -- PosInt(1)) shouldEqual PosInt(2)
27+
}
28+
29+
it should "work in Arrays" in {
30+
object Foo extends Subopaque.Default[Int]
31+
32+
val foo = Foo(42)
33+
Array(foo).apply(0) shouldEqual foo
34+
}
35+
36+
"Subopaque.Default" should "automatically create an apply method" in {
37+
object PersonId extends Subopaque.Default[Int]
38+
PersonId(1) shouldEqual 1
39+
}
40+
}
41+
42+
object SubopaqueTest {
43+
object PosInt extends Subopaque[Int] {
44+
def apply(value: Int): Type = wrap {
45+
if (value < 0) 0 else value
46+
}
47+
48+
implicit final class Ops(private val me: PosInt.Type) extends AnyVal {
49+
def --(other: PosInt.Type): PosInt.Type = wrap {
50+
val result = unwrap(me) - unwrap(other)
51+
if (result < 0) 0 else result
52+
}
53+
}
54+
}
55+
}

0 commit comments

Comments
 (0)