Opaque type problems: overriding does not work?

I wanted to extend the logarithmic representation example from the Scala3 book (here: Opaque Types | Scala 3 ā€” Book | Scala Documentation ) with toString:

object Logarithms:
//vvvvvv this is the important difference!
  opaque type Logarithm = Double

  object Logarithm:
    def apply(d: Double): Logarithm = math.log(d)

  extension (x: Logarithm)
    def toDouble: Double = math.exp(x)
    def + (y: Logarithm): Logarithm = x + math.log1p(math.exp(y-x))
    def * (y: Logarithm): Logarithm = x + y
    //  vvvvvvvvv these two do not work!
    //def toString: String = "log:"+x.asInstanceOf[Logarithm].toDouble.toString
    def toString2: String = "log:"+x.asInstanceOf[Logarithm].toDouble.toString
    //def toString2a: String = "log:"+x.toDouble.toString

    // this works, but this is not what I want
    def toDouble2: Double = math.exp(x)
    def toString3: String = "log:"+x.toDouble2.toString
    // this does not work:
    //def toString: String = "log:"+x.toDouble2.toString

It looks like method overriding does not work:

$ ~/Downloads/scala3-3.1.3/bin/scala
Welcome to Scala 3.1.3 (14.0.1, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.
                                                                                                                               
scala> :load my/scala/logarithms.scala
// defined object Logarithms
                                                                                                                               
scala> import Logarithms.*
                                                                                                                               
scala> Logarithm(4)
val res0: Logarithms.Logarithm = 1.3862943611198906
                                                                                                                               
scala> Logarithm(4).toString
val res1: String = 1.3862943611198906  // expected: log:4.0
                                                                                                                               
scala> Logarithm(4).toDouble.toString
val res2: String = 4.0
                                                                                                                               
scala> Logarithm(4).toString2
val res3: String = log:1.3862943611198906  // expected: log:4.0
                                                                                                                               
scala> Logarithm(4).toString3
val res4: String = log:4.0  // as expected, but there is no overriding: toDouble2 and toString3 !

It looks like one or two bugs. Or a leaky abstraction.
Or may be there is some syntax not covered in the Scala-3 book.

Hello, while this is not written explicitly there are two rules that make this manifest:

  • extension methods never replace a method already defined on the type
  • opaque types are subtypes of Any, so toString is already defined.

in the future there is [Edit: a very thin possibility] to move methods like toString to Matchable, which opaque types are not automatically a subtype of, which could make this possible.

If you would like to open a PR to the Scala 3 Book to summarise this point we would appreciate it.

I thought the reason Matchable was introduced instead of making Any a true parametric top type ā€“ or introducing a new Top type above Any ā€“ was that this would not be feasible in practice.

1 Like

I agree its pretty a slim chance, so I edited my answer

Technically speaking, could toString (and other) be removed from Any and provided through inlined extension methods? I imagine that would allow a more specific extension method to take precedence.

However I imagine interop with Java (String.format, printf etc.) would still suffer so Iā€™m not sure any of the proposed solutions would be better than status quo.