Proposal for Opaque Type Aliases

Fair enough, how about this:

opaque Permission { i: Int =>
}

I see that now, and I can’t say I’ve thought about it enough to make an opinion. My first observation though is that it’s unavoidable that opaques could be “upcasted” to Any, as type parameters act the same way:

trait Seq[A] {
  def head: A
  def isHeadInt: Boolean = head match {
    case i: Int => true
    case _ => false
  }
  def contains(elem: Any): Boolean = exists(_ == elem)
}

val permissions = Seq(Permission(0), Permission(2))
permissions.isHeadInt() // true
permissions.contains(0) // true
permissions.contains("lala") // false

And obviously there’s hashcode as well.

But they can, since all other types conform to the contract of Any, so they can rely on their wrapped types to fulfill that contract for them.

I don’t particularity like having a keyword followed by a parameters clause; they usually come only after an identifier — class name, function name, etc. How about this instead?

opaque Permission { i: Int =>
}

Just use the old syntax until we sort that thing out :slight_smile:

1 Like

Fair enough, and your proposal is close enough to self typing (and serves the same purpose), that it’s easy to reason about what it’s doing.

The question then becomes, how to we create these?

Is Permission.apply(Int) only created if there’s a synthetic companion object, and should it be created manually if a companion object is created by the programmer?

If Permission.apply(Int) isn’t created, what would it look like? It feels like it shouldn’t look like creating a class wrapper, as new Permission(4) looks way too much like what this isn’t. On the other hand, Permission(4) is a bit odd if you’re defining Permission.apply(Int) because you want to add some validation.

I’m guessing it could follow the rules of case classes. Is there any disadvantage to that? (it’s late and I can’t think clearly anymore)

I just realized how irregular this is, and I’m actually surprised that this compiles. How come an object can have abstract definitions?

1 Like

12 posts were split to a new topic: What’s in a type alias

The phrase “run-time semantics of a type alias” make me a bit uncomfortable – I never consider types to exist at run-time, so they can’t have any run-time semantics AFAIC. Is this the wrong intuition for opaque types or type aliases?

Your language has run-time semantics. It’s not because something disappears in the JVM bytecode that they don’t have run-time semantics. It’s just that the compiler encodes those run-time semantics in a way that does require leaving a definition for the type in the bytecode. But just the fact that the compiler leaves operations on those things in the bytecode means that something about them gets executed, so they have run-time semantics.

It’s not a wrong intuition for opaque types or type aliases. It’s a wrong intuition for the whole language, if you reason that anything that does not have a definition in the bytecode does not have run-time semantics.

Liberate yourself from thinking about the JVM. Think about what your source language means. When you code in Scala, do you always play the compiler to the JVM to reason about what your code does? Of course not: you reason about your code at the level of Scala. And that means you associate run-time semantics to the Scala language; not with the bytecode. The bytecode is an implementation detail.

1 Like

I don’t think I understand how types have run-time semantics at all. I’ll leave the discussion to those who do, while I question my understanding of the language.

OK maybe we don’t agree on some core definitions. Let me state it in a different way: every time I talk about the run-time semantics of a type, consider that I talk about the run-time semantics of core operations on that type. Core operations on a type T include x.isInstanceOf[T], x.asInstanceOf[T], classOf[T], since they are defined by the language spec depending on what T is. For example, the run-time semantics of x.asInstanceOf[T] when T is an opaque type T = X are aways equivalent to x.asInstanceOf[X].

I feel that this discussion on what is an abstract type member, what’s concrete, what’s bound, what’s inhabited, etc. is digressing from the topic of opaque type aliases.

I suggest to close this inconclusive digression here, unless it really brings something in terms of evaluating the opaque type alias proposal.

Edit: the mentioned discussion was moved to What's in a type alias

1 Like

You’re right. Back on topic, why not use a more concise syntax that is tailored to opaques?

After all, an opaque has absolutely no use if it doesn’t have associated extension methods as well:

object a {
  opaque type b = Int // completely useless
}

Also, it seems that opaques are already possible via existing syntax, and the currently proposed syntax does not really go all the way in reducing boilerplate.

What is your opinion on the syntax proposed earlier?

opaque Permission { i: Int =>
  def this(s: String): Permission = ???
  def foo(i : Permission): Permission = ???
}

object Permission {
  val NoPermission = Permission(0)
}

After all, an opaque has absolutely no use if it doesn’t have associated extension methods as well

Sorry to interfere, maybe I did not completely understand opaque types and their purposes but something like:

opaque type PositiveDouble <: Double = Double
object PositiveDouble
  def safe(double: Double): Option[PositiveDouble] =
    if (double > 0.0d) then Some(double)
    else None

Even if very naive doesn’t involve extension methods does it? (at least not as in: https://dotty.epfl.ch/docs/reference/contextual/extension-methods.html).

Do you mean “an associated object” or companion object or something like that? Is this the same as an extension method?
If so, I kinda agree, yes. For the opaque type not to be useless, you have to, at least, define a constructor (or I missed an obvious use case and it’d help me a lot if someone was pointing me at it), could be interesting to merge type declaration and such a definition, why not.

You are not interfering :slight_smile:

Much like any other type (say, classes), there isn’t much point of defining them if they are not associated with some public API – may it be via member methods or via extension methods (constructors not included).

The exception to this is marking; either types that have a fixed number of instances, each representing a different mark (those are enums); or traits that have no methods and are only used to “mark” other classes – which is a questionable design pattern but nonetheless impossible to do with opaques as their type (the “mark”) is erased at runtime.

If we take your exmple:

opaque type PositiveDouble <: Double = Double
object PositiveDouble
  def safe(double: Double): Option[PositiveDouble] =
    if (double > 0.0d) then Some(double)
    else None

It could be easily replaced with just a plain method:

object PositiveDoubleSimpler
  def safe(double: Double): Option[Double] = ... // same impl

As there isn’t much you can do with the original optional value:

PositiveDouble.safe(2.3).map { pd: PositiveDouble =>
  pd.??? // there is nothing you can do with `pd`
}

So really, an opaque type has no use if it doesn’t have some public API.

I disagree with the statement that an opaque type alias has no use if it doesn’t have a public API. It is perfectly reasonable to use an opaque type alias as a totally opaque (ah!) token, that you can give away to a user of your API, and that they can pass you back, at which point you open it again.

Concrete example: timer cancellation handle:

object Timers {
  opaque type CancellationHandle = Int

  def setTimeout(delay: Long)(op: => Unit): CancellationHandle = {
    val primitiveHandle: Int = SomeUnderlyingLib.setTimeout(delay, () => op)
    primitiveHandle // hidden away as CancellationHandle here
  }

  def cancelTimeout(handle: CancellationHandle): Unit = {
    val primitiveHandle: Int = handle // turned back into an Int here
    SomeUnderlyingLib.clearTimeout(primitiveHandle)
  }
}

With that, I have defined a useful opaque type alias that has no public API, not even explicit constructors and extractors.

7 Likes

That’s a good point. But then, this is quite a classic example of the wrapper pattern:

object Timers {
  opaque CancellationHandle { i: Int => }
  // or
  opaque CancellationHandle(i: Int)
  // or
  @opaque
  case class CancellationHandle(private[Timers] val i: Int)

  def setTimeout(delay: Long)(op: => Unit): CancellationHandle = {
    val primitiveHandle: Int = SomeUnderlyingLib.setTimeout(delay, () => op)
    CancellationHandle(primitiveHandle)
  }

  def cancelTimeout(handle: CancellationHandle): Unit = {
    val primitiveHandle: Int = handle.i
    SomeUnderlyingLib.clearTimeout(primitiveHandle)
  }
}
1 Like

We might have a problem with monad transformers and I’m pretty sure that it isn’t addressed by the opaque type proposal, if the proposal does nothing more than what we are already doing manually.

Classic example of a monad transformer is EitherT and we’ve got its flatMap defined to have some flexibility via an extra AA >: A type parameter (for auto widening):

case class EitherT[F[_], A, B](value: F[Either[A, B]])

  def flatMap[AA >: A, D](f: B => EitherT[F, AA, D])(implicit F: Monad[F]): EitherT[F, AA, D] = ???
}

When we implement a FlatMap[EitherT[F, E, *]], the implemented flatMap doesn’t have this flexibility:

implicit def instance[F[_], E](implicit F: FlatMap[F]) = 
  new FlatMap[EitherT[F, E, *]] {
    def flatMap[D](f: B => EitherT[F, A, D]): EitherT[F, A, D] = ???
  }

We have no AA >: A here. Meaning that when we do this:

// Import that brings the "syntax" in scope
import cats.implicits._

… we rely on the fact that the flatMap method defined on the case class itself has priority over the implicits that give us the flatMap syntax!

ALAS this stops being true with a type alias encoding that relies on extension methods, because those extension methods have the same priority as any other implicits and an “import cats.implicits._”, because it is an explicit import, will no longer allow you to use the right flatMap for EitherT.

Or in other words, this will no longer work:

EitherT.rightT[IO, Nothing](1).flatMap { x =>
  // Error! No more automatic widening for you ;-)
  EitherT.right[Throwable](IO(x + 1))
}

To simplify the problem a bit, for you all to understand it, the problem we have is that this:

case class Box[A](value: A) {
  def show: String =
    value.toString
}

Is not equivalent with this:

opaque type Box[A] = A

object Box {

  def apply[A](a: A) = a.asInstanceOf[Box[A]]

  implicit class Methods[A](self: Box[A]) extends AnyVal {
    def show: String = self.asInstanceOf[A].toString
  }
}

Because we can have this alternative definition for that method:

object Helpers {
  implicit class Methods2[A](self: Box[A]) extends AnyVal {
    def show: String = "¯\_(ツ)_/¯"
  }
}

And when we bring it in scope like this, then it’s going to override the opaque type’s own extension methods, something which doesn’t happen with a proper class:

import Helpers._

// Oops
Box(1).show
//=> ¯\_(ツ)_/¯

Which doesn’t seem so bad, except for the fact that the signature of the function itself can change:

object Helpers {
  implicit class Methods2[A](val self: Box[A]) extends AnyVal {

    def show: Int = Int.MaxValue

  }
}

And now we can end up with a compile error, depending on whether that method was defined as an extension method for an opaque type or a an actual method of a proper class:

import Helpers._
val str: String = Box(1).show
                         ^
error: type mismatch;
found   : Int
required: String

This is one case in which a proper class is very clearly not behaving like a type + extension methods at compile time!

And this is very problematic for us if we want to define monad transformers, while keeping our cats.syntax.

1 Like

I have the suspicion that this has more to do with implicits than opaques, but I couldn’t fully understand the example (mind you that I’m not very familiar with Cats).

  1. Why is there more than one implementation of flatMap?

  2. What’s with the E and * type parameters in the implicit def instance? Also the A there seem to come out of nowhere and refer to nothing.

Not sure I understand, the two are very intertwined and how the extension methods for the opaque types get solved and how they interact with other implicits is very relevant, being part of the SIP proposal.

It’s a real-world scenario, EitherT is more comfortable to use if automatic type widening happens in its flatMap, but the Monad type-class doesn’t support it due to its (single) type parameter.

In instances in which the Monad[F] interface is being used, this doesn’t matter, where it does matter is when we use the extension methods provided automatically for all types implementing Monad[F] (that people bring in scope via import cats.implicits._). Which currently aren’t a problem for as long as EitherT is a proper class, due to how the compiler solves those method calls.

This isn’t related only to EitherT btw. Scala’s collections too have a different signature made for ease of use, see for example List.

E is for the error type, you need one because the Either[E, A] has two type parameters (left and right). * is a lambda projection, syntactic sugar currently made possible by the kind-projector compiler plugin, so these definitions would be equivalent:

new FlatMap[EitherT[F, E, *]] {}

new FlatMap[{type L[A]=EitherT[F, E, A]}#L] {}

type L[A] = EitherT[F, E, A]
new FlatMap[L] {}

And we need to do this because FlatMap[Either] and FlatMap[EitherT] aren’t possible due to Scala not doing type parameters currying and partial application, like Haskell does, hence extra syntax.

Bad copy/paste job on my part, A is the type representing errors ¯_(ツ)_/¯ that shouldn’t detract from the main message though.

Hope that helps. Cheers,

1 Like

Because if I understand the scenario correctly (and I may very well not be), the problem stems from the encoding using classes, type-classes and extensions, and it remained silent until it manifested with opaques.

If I got it right, this is a more simplistic example of the scenario and the problem at hand:

trait TypeClass[A] {
  def foo(a: A): Int
}

object TypeClass {
  implicit class TypeClassOps[A](a: A)(implicit tc: TypeClass[A]) {
      def foo(): Int = tc.foo(a)
  }
}

class Transformer {
  def foo(): Int = 1
}

object Transformer {
  implicit object TransformerTypeClass extends TypeClass[Transformer] {
      def foo(t: Transformer): Int = 2
  }
}

Where TypeClass is Monad and Transformer is EitherT, and there is a need for to different implementations of the method foo (in this example they behave differently just for clarity, but I understand that they should behave the same in practice). I left out the “derivation” of the transformer according to the type-instance of its type parameter, as it seemed irrelevant and only complicates the example (correct me if I’m wrong).

If you ask my opinion, this is an abuse of Scala’s features which creates great ambiguity and confusion among users. It signifies that on one hand flatMap is an intrinsic part of EitherT, but on the other hand it’s a generic behavior that EitherT happens to exhibit – and being a generic behavior it is not tailored to what EitherT really is. This is somewhat equivalent to:

trait Behavior {
  def foo(): String
}

// this obviously doesn't compile
class Transformer extends Behavior {
  override def foo(): String = foo().toString
  def foo(): Int = 0
}

I would perhaps extract both methods from the class as extension methods, place them in separate objects (not companions of the transformer), and ask users to import the method they want to be using (the generic one or the transformer-specific one).

The other alternative would be to introduce a conflict-resolution mechanism for implicits – for instance, lenses – but that is a general issue regarding implicits and not opaques.

I wouldn’t necessarily refer to Scala’s collections as a good design.