Extension on opaque type not seen in the scope of the type

In following code, the extension show cannot be used in the scope where the opaque type Vec is declared, but it can be used when the type is imported into another scope. Why is this so, and can something be done so that the extension can be used there? I cannot use import Vec.show in the scope, as there are other types in the same scope with the show method, and importing all of them would lead to a conflict.

Using implicit class instead of extension shows the same behavior, with the difference that importing as import Vec.* is possible as a workaround.

object Types {
  case class V(x: Double, y: Double)

  opaque type Vec = V

  object Vec {
    def apply(x: Double, y: Double): Vec = V(x, y)

    extension (v: Vec) {
      def show: String = v.toString
    }
  }

  val v = Vec(1, 2)
  //v.show // does not work
}

import Types.Vec

val v = Vec(1, 2)
v.show // works
1 Like

Just a guess from looking at the doc here

object Types:
  case class V(x: Double, y: Double)
  object V:
    extension (v: V)
      private def ops = new Ops(v)
      export ops.*
  class Ops(v: V):
    def show = v.toString

  opaque type Vec = V

  object Vec:
    def apply(x: Double, y: Double): Vec = V(x, y)

    extension (v: Vec)
      private def ops = new Ops(v)
      export ops.*

  def test =
    val v = Vec(1, 2)
    v.show
end Types

@main def test() = println:
  import Types.Vec

  val v = Vec(1, 2)
  // works
  (v.show, Types.test)

I think your question is a FAQ but I can’t find discussion.

1 Like

I am not sure I understand this. How does this explain why show does not work in the scope of the opaque type - or is that supposed to be obvious?

Is this a workaround, suggesting I should wrap my extensions in helper cases for this to work? In that case I guess I could use implicit class instead, with the same overhead - but still, I am more interested in “why”.

In the scope of the opaque type, it’s just a non-opaque type alias per the doc.

That doc says you want

opaque type N = Int
extension (x: N) def +(y: N) = x + y

to do the obvious thing and not loop.

The extension is the “external” API, and internally you implement it in terms of operations on Int.

Note that you can always call Types.+(x)(y), using a dot to avoid unary plus.

The syntax x.+(y) uses x: Int and not the alias type.

Also there is “relaxed imports” for extension methods, which allow for overloading resolution of imported methods of the same name.

object Types {
  case class V(x: Double, y: Double)

  opaque type Vec = V

  object Vec {
    def apply(x: Double, y: Double): Vec = V(x, y)

    extension (v: Vec) {
      def show: String = v.toString
    }
  }
  class C
  object D:
    extension (c: C) def show = "C!"

  def test =
    import D.show
    import Types.Vec.show
    val v = Vec(1, 2)
    val d = C()
    (v.show, d.show)
}

@main def test() = println:
  import Types.Vec

  val v = Vec(1, 2)
  (v.show, Types.test)

Worth adding there’s a note with differences from the SIP, including apparently that originally the alias was opaque only in its “companion”, and that was changed to include the scope where it’s defined. There is probably further discussion at the SIP on the deep meaning.

1 Like

Thanks, that starts to make sense to me now, assuming you meant to say non-opaque here.

I am not sure where can I read about the real SIP as it was eventually implemented. The public version reads:

https://docs.scala-lang.org/sips/opaque-types.html

The key peculiarity of opaque types is that they behave like normal type aliases inside their type companion; that is, users can convert from the type alias and its equivalent definition interchangeably without the use of explicit lift and unlift methods. We can say that opaque types are “transparent” inside their type companion.

However, the story changes for users of this API. Outside of their type companions, opaque types are not transparent

I guess one is supposed to read Opaque Type Aliases: More Details

At the bottom it mentions:

The scope where an opaque type alias is visible is now the whole scope where it is defined, instead of just a companion object.

There is no rationale, though, and no links anywhere one could find it.

There is also another confusing point:

The notion of a companion object for opaque type aliases has been dropped.

While the accompanying object is probably technically not a companion object, for many purposes, esp. extension, implicit, and given(?) lookup it seems to act as one.

Indeed I think everyone has found those two behaviors confusing and not a single time they have been useful IME.
Yet, that is what they implemented.

I do not think a deliberate decision was made so that the behaviour is annoying. There is probably some reason behind this, and I think knowing the reason would make bearing the consequences easier.

The opaque’s object is called an “anchor” in the reference about implicit scope.

That explains its special behavior you noted.

I think the “scopedness” of the opaque type is most confusing when it is top-level in a file, because the definition becomes scoped to the “file package object”.

So other top-level defs such as methods are in the scope where it is a “transparent” alias, but top-level classes see it as opaque.

Thanks for your understanding about my conflation of opaque and non-opaque yesterday. I think language needs a marker for “X or maybe its opposite”. Well, we have words like “irregardless” where a prefix means “intensifier not opposite, are you paying attention to what I’m trying to say”, irregardless.

1 Like

I have consulted the text with my (inner) language lawyer and he told me it is the opaque type itself which is called an anchor, the object is merely a part of implicit scope:

A reference is an anchor if it refers to an… an opaque type alias … Opaque type aliases count as anchors only outside the scope where their alias is visible.

The implicit scope of a type T

If T is a reference to an opaque type alias named A , S includes a reference to an object A defined in the same scope as the type, if it exists,

Still, thanks for point out where the behavior is defined, I appreciate that.

Irregardless, I meant one of two things.

The boolean blindness of black-and-white terms!