Skip to content

Commit 641dbba

Browse files
nonjvican
authored andcommitted
Add some more examples of opaque types. (#904)
* Add some more examples of opaque types. I still wanted to write more, but here's what I have at this moment. * Updates. This adds some prose to each example, and adds the `Nullable` example. * Small edit.
1 parent d337572 commit 641dbba

File tree

1 file changed

+361
-1
lines changed

1 file changed

+361
-1
lines changed

_sips/sips/2017-09-20-opaque-types.md

Lines changed: 361 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ val l: Logarithm = 1.0
221221

222222
which fails to compile with a type mismatch error:
223223

224-
```scala
224+
```
225225
<console>:11: error: type mismatch;
226226
found : Double
227227
required: Logarithm
@@ -376,6 +376,366 @@ The extension methods synthesized for implicit value classes are not static. We
376376
if the current encoding has an impact in performance, but if so, we can consider changing the
377377
encoding of extension methods either in this proposal or in a future one.
378378

379+
## More examples
380+
381+
### Type tagging
382+
383+
This example focuses on using type parameters to opaque types (one of
384+
which is a phantom type). The goal here is to be able to mark concrete
385+
types (`S`) with arbitrary *tags* (`T`) which can be used to
386+
distinguish them at compile-time.
387+
388+
```scala
389+
package object tagging {
390+
391+
// Tagged[S, T] means that S is tagged with T
392+
opaque type Tagged[S, T] = S
393+
394+
object Tagged {
395+
def tag[S, T](s: S): Tagged[S, T] = s
396+
def untag[S, T](st: Tagged[S, T]): S = st
397+
398+
def tags[F[_], S, T](fs: F[S]): F[Tagged[S, T]] = fs
399+
def untags[F[_], S, T](fst: F[Tagged[S, T]]): F[S] = fst
400+
401+
implicit def taggedClassTag[S, T](implicit ct: ClassTag[S]): ClassTag[Tagged[S, T]] =
402+
ct
403+
}
404+
405+
type @@[S, T] = Tagged[S, T]
406+
407+
implicit class UntagOps[S, T](st: S @@ T) extends AnyVal {
408+
def untag: S = Tagged.untag(st)
409+
}
410+
411+
implicit class UntagsOps[F[_], S, T](fs: F[S @@ T]) extends AnyVal {
412+
def untags: F[S] = Tagged.untags(fs)
413+
}
414+
415+
implicit class TagOps[S](s: S) extends AnyVal {
416+
def tag[T]: S @@ T = Tagged.tag(s)
417+
}
418+
419+
implicit class TagsOps[F[_], S](fs: F[S]) extends AnyVal {
420+
def tags[T]: F[S @@ T] = Tagged.tags(fs)
421+
}
422+
423+
trait Meter
424+
trait Foot
425+
trait Fathom
426+
427+
val x: Double @@ Meter = (1e7).tag[Meter]
428+
val y: Double @@ Foot = (123.0).tag[Foot]
429+
val xs: Array[Double @@ Meter] = Array(1.0, 2.0, 3.0).tags[Meter]
430+
431+
val o: Ordering[Double] = implicitly
432+
val om: Ordering[Double @@ Meter] = o.tags[Meter]
433+
om.compare(x, x) // 0
434+
om.compare(x, y) // does not compile
435+
xs.min(om) // 1.0
436+
xs.min(o) // does not compile
437+
438+
// uses ClassTag[Double] via 'Tagged.taggedClassTag'.
439+
val ys = new Array[Double @@ Foot](20)
440+
}
441+
```
442+
443+
There are a few interesting things to note here.
444+
445+
First, as above we expect that tagging and untagging will not cause
446+
any boxing of these values at runtime, even though `Tagged` is
447+
generic. We also expect that the `Array[Double @@ Meter]` will be
448+
represented by `Array[Double]` at runtime.
449+
450+
Second, notice that `Ordering[Double]` and `ClassTag[Double]` are not
451+
automatically in scope for `Tagged[Double, Meter]`. Opaque types
452+
currently need to "re-export" (or otherwise provide) their own
453+
implicit values.
454+
455+
It would be possible to automatically provide `ClassTag` instances,
456+
using an `implicit val` in the case of opaque types wrapping concrete
457+
types (e.g.`opaque type X = Double`) and `implicit def` in cases such
458+
as above.
459+
460+
### Fix-point type
461+
462+
Here's an interesting little example which defines the recursive
463+
opaque type `Fix`:
464+
465+
```scala
466+
package object fixed {
467+
opaque type Fix[F_]] = F[Fix[F]]
468+
469+
object Fix {
470+
def fix[F[_]](unfixed: F[Fix[F]]): Fix[F] = unfixed
471+
def unfix[F[_]](fixed: Fix[F]): F[Fix[F]] = fixed
472+
}
473+
474+
sealed abstract class TreeU[R]
475+
476+
type Tree = Fix[TreeU]
477+
478+
object TreeU {
479+
def cata[A](t: Tree)(f: TreeU[A] => A): A =
480+
f(Fix.unfix(t) match {
481+
case Branch(l, r) => Branch(cata(l)(f), cata(r)(f))
482+
case Leaf(s) => Leaf(s)
483+
})
484+
485+
case class Branch[R](left: R, right: R) extends TreeU[R]
486+
case class Leaf[R](label: String) extends TreeU[R]
487+
}
488+
489+
def leaf(s: String): Tree = Fix.fix(Leaf(s))
490+
def branch(lhs: Tree, rhs: Tree): Tree = Fix.fix(Branch(lhs, rhs))
491+
492+
val tree: Tree = branch(branch(leaf("a"), leaf("b")), leaf("c"))
493+
494+
println(tree)
495+
// Branch(Branch(Leaf(a), Leaf(b)), Leaf(c))
496+
}
497+
```
498+
499+
This is an interesting example which is intended to show that opaque
500+
types (unlike type aliases) have an independent existence, even within
501+
the companion. This means that unlike type alises, `Fix[F]` should not
502+
result in an infinite expansion in the above code.
503+
504+
The `Fix` type is useful to implementing recursion schemes, or just
505+
for creating recursive structures which are parameterized on their
506+
recursive type.
507+
508+
### Explicit nullable types
509+
510+
There have previously been proposals to provide a "zero-cost Option"
511+
using value classes. Opaque types make this very straight-forward by
512+
bounding the underlying type (`A`) with `AnyRef`.
513+
514+
```scala
515+
package object nullable {
516+
opaque type Nullable[A <: AnyRef] = A
517+
518+
object Nullable {
519+
def apply[A <: AnyRef](a: A): Nullable[A] = a
520+
521+
implicit class NullableOps[A <: AnyRef](na: Nullable[A]) {
522+
def exists(p: A => Boolean): Boolean =
523+
na != null && p(na)
524+
def filter(p: A => Boolean): Nullable[A] =
525+
if (na != null && p(na)) na else null
526+
def flatMap[B <: AnyRef](f: A => Nullable[B]): Nullable[B] =
527+
if (na == null) null else f(na)
528+
def forall(p: A => Boolean): Boolean =
529+
na == null || p(na)
530+
def getOrElse(a: => A): A =
531+
if (na == null) a else na
532+
def map[B <: AnyRef](f: A => B): Nullable[B] =
533+
if (na == null) null else f(na)
534+
def orElse(na2: => Nullable[A]): Nullable[A] =
535+
if (na == null) na2 else na
536+
def toOption: Option[A] =
537+
Option(na)
538+
}
539+
}
540+
}
541+
```
542+
543+
This example provides most of the benefits of using `Option` at API
544+
boundaries with libraries that use `null` (such as many Java
545+
libraries). Unlike a value class, we can guarantee that there will
546+
never be a wrapper around the raw values (or raw nulls).
547+
548+
Notice that `Nullable[Nullable[B]]` is not a valid type, because
549+
`Nullalbe[B]` is not known to be `<: AnyRef`. This is a key difference
550+
between a type like `Option` (which is parametric and can easily wrap
551+
itself) and a type like `Nullable` (which only has one `null` value to
552+
use).
553+
554+
### Custom instances
555+
556+
Currently, many libraries (including Scalding and Algebird) define
557+
wrapper types which change the kind of aggregation used for that
558+
type. This is useful in frameworks like MapReduce, Spark, Flink,
559+
Storm, etc. where users describe computation in terms of mapping and
560+
aggregation.
561+
562+
The following example shows how opaque types can make using these
563+
wrappers a bit more elegant:
564+
565+
```scala
566+
package object groups {
567+
trait Semigroup[A] {
568+
def combine(x: A, y: A): A
569+
}
570+
571+
object Semigroup {
572+
def instance[A](f: (A, A) => A): Semigroup[A] =
573+
new Semigroup[A] {
574+
def combine(x: A, y: A): A = f(x, y)
575+
}
576+
}
577+
578+
type Id[A] = A
579+
580+
trait Wrapping[F[_]] {
581+
def wraps[G[_], A](ga: G[A]): G[F[A]]
582+
def unwraps[G[_], A](ga: G[F[A]]): G[A]
583+
}
584+
585+
abstract class Wrapper[F[_]] { self =>
586+
def wraps[G[_], A](ga: G[A]): G[F[A]]
587+
def unwraps[G[_], A](gfa: G[F[A]]): G[A]
588+
589+
final def apply[A](a: A): F[A] = wraps[Id, A](a)
590+
591+
implicit object WrapperWrapping extends Wrapping[F] {
592+
def wraps[G[_], A](ga: G[A]): G[F[A]] = self.wraps(ga)
593+
def unwraps[G[_], A](ga: G[F[A]]): G[A] = self.unwraps(ga)
594+
}
595+
}
596+
597+
opaque type First[A] = A
598+
object First extends Wrapper[First] {
599+
def wraps[G[_], A](ga: G[A]): G[First[A]] = ga
600+
def unwrap[G[_], A](gfa: G[First[A]]): G[A] = gfa
601+
implicit def firstSemigroup[A]: Semigroup[First[A]] =
602+
Semigroup.instance((x, y) => x)
603+
}
604+
605+
opaque type Last[A] = A
606+
object Last extends Wrapper[Last] {
607+
def wraps[G[_], A](ga: G[A]): G[Last[A]] = ga
608+
def unwrap[G[_], A](gfa: G[Last[A]]): G[A] = gfa
609+
implicit def lastSemigroup[A]: Semigroup[Last[A]] =
610+
Semigroup.instance((x, y) => y)
611+
}
612+
613+
opaque type Min[A] = A
614+
object Min extends Wrapper[Min] {
615+
def wraps[G[_], A](ga: G[A]): G[Min[A]] = ga
616+
def unwrap[G[_], A](gfa: G[Min[A]]): G[A] = gfa
617+
implicit def minSemigroup[A](implicit o: Ordering[A]): Semigroup[Min[A]] =
618+
Semigroup.instance(o.min)
619+
}
620+
621+
opaque type Max[A] = A
622+
object Max extends Wrapper[Max] {
623+
def wraps[G[_], A](ga: G[A]): G[Max[A]] = ga
624+
def unwrap[G[_], A](gfa: G[Max[A]]): G[A] = gfa
625+
implicit def maxSemigroup[A](implicit o: Ordering[A]): Semigroup[Max[A]] =
626+
Semigroup.instance(o.max)
627+
}
628+
629+
opaque type Plus[A] = A
630+
object Plus extends Wrapper[Plus] {
631+
def wraps[G[_], A](ga: G[A]): G[Plus[A]] = ga
632+
def unwrap[G[_], A](gfa: G[Plus[A]]): G[A] = gfa
633+
implicit def plusSemigroup[A](implicit n: Numeric[A]): Semigroup[Plus[A]] =
634+
Semigroup.instance(n.plus)
635+
}
636+
637+
opaque type Times[A] = A
638+
object Times extends Wrapper[Times] {
639+
def wraps[G[_], A](ga: G[A]): G[Times[A]] = ga
640+
def unwrap[G[_], A](gfa: G[Times[A]]): G[A] = gfa
641+
implicit def timesSemigroup[A](implicit n: Numeric[A]): Semigroup[Times[A]] =
642+
Semigroup.instance(n.times)
643+
}
644+
645+
opaque type Reversed[A] = A
646+
object Reversed extends Wrapper[Reversed] {
647+
def wraps[G[_], A](ga: G[A]): G[Reversed[A]] = ga
648+
def unwrap[G[_], A](gfa: G[Reversed[A]]): G[A] = gfa
649+
implicit def reversedOrdering[A](implicit o: Ordering[A]): Ordering[Reversed[A]] =
650+
o.reverse
651+
}
652+
653+
opaque type Unordered[A] = A
654+
object Unordered extends Wrapper[Unordered] {
655+
def wraps[G[_], A](ga: G[A]): G[Reversed[A]] = ga
656+
def unwrap[G[_], A](gfa: G[Reversed[A]]): G[A] = gfa
657+
implicit def unorderedOrdering[A]: Ordering[Unordered[A]] =
658+
Ordering.by(_ => ())
659+
}
660+
}
661+
```
662+
663+
The example demonstrates using an abstract class (`Wrapper`) to share
664+
code between opaque type companion objects. Like the tagging example,
665+
we can use two methods (`wraps` and `unwraps`) to wrap and unwrap `A`
666+
types, even if neseted in an arbitrary context (`G[_]`). These methods
667+
cannot be implemented in `Wrapper` because each opaque type companion
668+
contains the only scope where its particular methods can be
669+
implemented.
670+
671+
Similarly to the tagging example, these types are zero-cost wrappers
672+
which can be used to tag how to aggregate the underlying type (for
673+
example `Int`).
674+
675+
### Probability interval
676+
677+
Here's an example that demonstrates how opaque types can limit the
678+
underlying type's range of values in a way that minimizes the require
679+
error-checking:
680+
681+
```scala
682+
package object prob {
683+
opaque type Probability = Double
684+
685+
object Probability {
686+
def apply(n: Double): Option[Probability] =
687+
if (0.0 <= n && n <= 1.0) Some(n) else None
688+
689+
def unsafe(p: Double): Probability = {
690+
require(0.0 <= p && p <= 1.0, s"probabilities lie in [0, 1] (got $p)")
691+
p
692+
}
693+
694+
def asDouble(p: Probability): Double = p
695+
696+
val Never: Probability = 0.0
697+
val CoinToss: Probability = 0.5
698+
val Certain: Probability = 1.0
699+
700+
implicit val ordering: Ordering[Probability] =
701+
implicitly[Ordering[Double]]
702+
703+
implicit class ProbabilityOps(p1: Probability) extends AnyVal {
704+
def unary_~ : Probability = Certain - p1
705+
def &(p2: Probability): Probability = p1 * p2
706+
def |(p2: Probability): Probability = p1 + p2 - (p1 * p2)
707+
708+
def isImpossible: Boolean = p1 == Never
709+
def isCertain: Boolean = p1 == Certain
710+
711+
import scala.util.Random
712+
713+
def sample(r: Random = Random): Boolean = r.nextDouble <= p1
714+
def toDouble: Double = p1
715+
}
716+
717+
val caughtTrain = Probability.unsafe(0.3)
718+
val missedTrain = ~caughtTrain
719+
val caughtCab = Probability.CoinToss
720+
val arrived = caughtTrain | (missedTrain & caughtCab)
721+
722+
println((1 to 5).map(_ => arrived.sample()).toList)
723+
// List(true, true, false, true, false)
724+
}
725+
}
726+
```
727+
728+
Outside of the `Probability` companion, we can be sure that the
729+
underlying `Double` values fall in the interval *[0, 1]*, which means
730+
we don't need to include error-checking code when working with
731+
`Probability` values. We can be sure that adding this kind of
732+
compile-time safety to a program doesn't add any additional cost
733+
(except for the error-checking that we explicitly want).
734+
735+
This example is somewhat similar to `Logarithm` above. Other
736+
properties we might want to verify at compile-time: `NonNegative`,
737+
`Positive`, `Finite`, `Unsigned` and so on.
738+
379739
## Implementation notes
380740

381741
To implement opaque types, we need to modify three compiler phases: parser, namer and typer. At the

0 commit comments

Comments
 (0)