-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add Opaque util to act Scala 3 Opaque Types
- Loading branch information
1 parent
5131c93
commit b6a5b95
Showing
7 changed files
with
214 additions
and
0 deletions.
There are no files selected for viewing
13 changes: 13 additions & 0 deletions
13
core/src/main/scala/com/avsystem/commons/opaque/BaseOpaque.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
17
core/src/main/scala/com/avsystem/commons/opaque/Castable.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
19
core/src/main/scala/com/avsystem/commons/opaque/Opaque.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
15
core/src/main/scala/com/avsystem/commons/opaque/Subopaque.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
43
core/src/test/scala/com/avsystem/commons/opaque/CastableTest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
52
core/src/test/scala/com/avsystem/commons/opaque/OpaqueTest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} | ||
} |
55 changes: 55 additions & 0 deletions
55
core/src/test/scala/com/avsystem/commons/opaque/SubopaqueTest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} | ||
} | ||
} |