Skip to content

Commit

Permalink
Merge pull request #14531 from som-snytt/tweak/opaque-example
Browse files Browse the repository at this point in the history
  • Loading branch information
dwijnand authored Mar 4, 2022
2 parents 482c67e + 93a7a3a commit 1b25f65
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 35 deletions.
36 changes: 24 additions & 12 deletions docs/_docs/reference/other-new-features/opaques.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ end MyMath

This introduces `Logarithm` as a new abstract type, which is implemented as `Double`.
The fact that `Logarithm` is the same as `Double` is only known in the scope where
`Logarithm` is defined which in the above example corresponds to the object `MyMath`.
Or in other words, within the scope it is treated as type alias, but this is opaque to the outside world
where in consequence `Logarithm` is seen as an abstract type and has nothing to do with `Double`.
`Logarithm` is defined, which in the above example corresponds to the object `MyMath`.
Or in other words, within the scope, it is treated as a type alias, but this is opaque to the outside world
where, in consequence, `Logarithm` is seen as an abstract type that has nothing to do with `Double`.

The public API of `Logarithm` consists of the `apply` and `safe` methods defined in the companion object.
They convert from `Double`s to `Logarithm` values. Moreover, an operation `toDouble` that converts the other way, and operations `+` and `*` are defined as extension methods on `Logarithm` values.
Expand Down Expand Up @@ -70,13 +70,12 @@ object Access:
opaque type PermissionChoice = Int
opaque type Permission <: Permissions & PermissionChoice = Int

extension (x: Permissions)
def & (y: Permissions): Permissions = x | y
extension (x: PermissionChoice)
def | (y: PermissionChoice): PermissionChoice = x | y
extension (x: Permissions)
def & (y: Permissions): Permissions = x | y
extension (granted: Permissions)
def is(required: Permissions) = (granted & required) == required
extension (granted: Permissions)
def isOneOf(required: PermissionChoice) = (granted & required) != 0

val NoPermission: Permission = 0
Expand All @@ -101,9 +100,12 @@ where `x | y` means "a permission in `x` *or* in `y` granted".

Note that inside the `Access` object, the `&` and `|` operators always resolve to the corresponding methods of `Int`,
because members always take precedence over extension methods.
Because of that, the `|` extension method in `Access` does not cause infinite recursion.
Also, the definition of `ReadWrite` must use `|`,
even though an equivalent definition outside `Access` would use `&`.
For that reason, the `|` extension method in `Access` does not cause infinite recursion.

In particular, the definition of `ReadWrite` must use `|`, the bitwise operator for `Int`,
even though client code outside `Access` would use `&`, the extension method on `Permissions`.
The internal representations of `ReadWrite` and `ReadOrWrite` are identical, but this is not visible to the client,
which is interested only in the semantics of `Permissions`, as in the example below.

All three opaque type aliases have the same underlying representation type `Int`. The
`Permission` type has an upper bound `Permissions & PermissionChoice`. This makes
Expand All @@ -115,8 +117,11 @@ object User:
import Access.*

case class Item(rights: Permissions)
extension (item: Item)
def +(other: Item): Item = Item(item.rights & other.rights)

val roItem = Item(Read) // OK, since Permission <: Permissions
val woItem = Item(Write)
val rwItem = Item(ReadWrite)
val noItem = Item(NoPermission)

Expand All @@ -128,11 +133,18 @@ object User:

assert(!noItem.rights.is(ReadWrite))
assert(!noItem.rights.isOneOf(ReadOrWrite))

assert((roItem + woItem).rights.is(ReadWrite))
end User
```

On the other hand, the call `roItem.rights.isOneOf(ReadWrite)` would give a type error
since `Permissions` and `PermissionChoice` are different, unrelated types outside `Access`.
On the other hand, the call `roItem.rights.isOneOf(ReadWrite)` would give a type error:
```scala
assert(roItem.rights.isOneOf(ReadWrite))
^^^^^^^^^
Found: (Access.ReadWrite : Access.Permissions)
Required: Access.PermissionChoice
```
`Permissions` and `PermissionChoice` are different, unrelated types outside `Access`.


### Opaque Type Members on Classes
Expand Down
53 changes: 30 additions & 23 deletions tests/pos/reference/opaque.scala
Original file line number Diff line number Diff line change
@@ -1,77 +1,84 @@
object Logarithms {
object MyMath:

opaque type Logarithm = Double

object Logarithm {
object Logarithm:

// These are the two ways to lift to the Logarithm type

// These are the ways to lift to the logarithm type
def apply(d: Double): Logarithm = math.log(d)

def safe(d: Double): Option[Logarithm] =
if (d > 0.0) Some(math.log(d)) else None
}
if d > 0.0 then Some(math.log(d)) else None

end Logarithm

// Extension methods define opaque types' public APIs
extension (x: Logarithm)
def toDouble: Double = math.exp(x)
def + (y: Logarithm): Logarithm = Logarithm(math.exp(x) + math.exp(y))
def * (y: Logarithm): Logarithm = x + y
}

object LogTest {
import Logarithms.*
end MyMath

object LogTest:
import MyMath.Logarithm
import Predef.{any2stringadd as _, *}

val l = Logarithm(1.0)
val l2 = Logarithm(2.0)
val l3 = l * l2
val l4 = l + l2
}


object Access {

object Access:

opaque type Permissions = Int
opaque type PermissionChoice = Int
opaque type Permission <: Permissions & PermissionChoice = Int

extension (x: Permissions)
def & (y: Permissions): Permissions = x | y
extension (x: PermissionChoice)
def | (y: PermissionChoice): PermissionChoice = x | y
extension (x: Permissions)
def & (y: Permissions): Permissions = x | y
extension (granted: Permissions)
def is(required: Permissions) = (granted & required) == required
extension (granted: Permissions)
def isOneOf(required: PermissionChoice) = (granted & required) != 0

val NoPermission: Permission = 0
val Read: Permission = 1
val Write: Permission = 2
val ReadWrite: Permissions = Read | Write
val ReadOrWrite: PermissionChoice = Read | Write
}

object User {
end Access

object User:
import Access.*

case class Item(rights: Permissions)
extension (item: Item)
def +(other: Item): Item = Item(item.rights & other.rights)

val roItem = Item(Read) // OK, since Permission <: Permissions
val woItem = Item(Write)
val rwItem = Item(ReadWrite)
val noItem = Item(NoPermission)

assert( roItem.rights.is(ReadWrite) == false )
assert( roItem.rights.isOneOf(ReadOrWrite) == true )
assert(!roItem.rights.is(ReadWrite))
assert(roItem.rights.isOneOf(ReadOrWrite))

assert( rwItem.rights.is(ReadWrite) == true )
assert( rwItem.rights.isOneOf(ReadOrWrite) == true )
assert(rwItem.rights.is(ReadWrite))
assert(rwItem.rights.isOneOf(ReadOrWrite))

assert( noItem.rights.is(ReadWrite) == false )
assert( noItem.rights.isOneOf(ReadOrWrite) == false )
assert(!noItem.rights.is(ReadWrite))
assert(!noItem.rights.isOneOf(ReadOrWrite))

assert((roItem + woItem).rights.is(ReadWrite))
// Would be a type error:
// assert( roItem.rights.isOneOf(ReadWrite) == true )
}
// assert(roItem.rights.isOneOf(ReadWrite))
end User

object o {
opaque type T = Int
Expand Down

0 comments on commit 1b25f65

Please sign in to comment.