estatico / scala-newtype

NewTypes for Scala with no runtime overhead
Apache License 2.0
540 stars 31 forks source link

"+" method shadowed by any2stringadd #65

Closed kubukoz closed 4 years ago

kubukoz commented 4 years ago

Hi! Thanks for a wonderful library.

We're hitting an interesting issue:

@newtype case class Demo(value: Int) {
  def +(another: Demo): Demo = ???
}

def demo(d: Demo) = d + d
                      ^ type mismatch;
 found   : (...).Demo
    (which expands to)  (...).Demo.Type
 required: String

If I just say d +, I get missing argument list for method + in class any2stringadd. As far as I could find out, that method gets special treatment and can't be unimported.

I took a look at the implementation of newtypes and as far as I could tell methods from the class are moved to an implicit class next to it, to which a conversion called Demo.Ops$newtype is applied - when I apply it manually, or import it (import Demo._ or by exact name) it compiles as expected. I assume any2stringadd just has higher precedence because it's imported.

Is there something scala-newtype can do to allow this name in methods on newtypes?

joroKr21 commented 4 years ago

You can unimport it: import Predef.{any2stringadd => _}. And you can use -Yimports project-wide.

kubukoz commented 4 years ago

Nope, that still shows the same type mismatch error... and -Yimports isn't an option in 2.12 which I'm currently stuck on :(

joroKr21 commented 4 years ago

Do you get something else? 🤔

❯ scala
Welcome to Scala 2.12.11 (OpenJDK 64-Bit Server VM, Java 12.0.2).
Type in expressions for evaluation. Or try :help.

scala> object A
defined object A

scala> A + ""
res0: String = A$@2d119405

scala> import Predef.{any2stringadd => _}
import Predef.{any2stringadd=>_}

scala> A + ""
<console>:14: error: value + is not a member of object A
       A + ""
         ^
kubukoz commented 4 years ago

I get the same in the REPL, but not in the normal compilations. I think that's on purpose: https://github.com/scala/scala/pull/4554

joroKr21 commented 4 years ago

This also doesn't compile for me:

import Predef.{any2stringadd => _}

object Scratch {
  object A
  A + ""
}
kubukoz commented 4 years ago

Okay, I actually hadn't checked that (just extrapolated a bit from the newtype example, sorry!).

That works. But not on a newtype...

carymrobbins commented 4 years ago

This is an annoying problem with Scala's implicit resolution precedence (and stdlib). Here are three ways around it.

First, @joroKr21's solution seems to work for me -

import Predef.{any2stringadd => _}
import io.estatico.newtype.macros.newtype

object Main {

  @newtype case class Demo(value: Int) {
    def +(another: Demo): Demo = Demo(value + another.value)
  }

  def main(args: Array[String]): Unit = {
    val demo: Demo = Demo(1) + Demo(2)
    System.out.println(demo)
  }
}

Second, create your own implicit which has higher precedence than any2stringadd -

import io.estatico.newtype.macros.newtype

object Main {

  @newtype case class Demo(value: Int)

  implicit final class Ops(private val demo: Demo) extends AnyVal {
    def +(another: Demo): Demo = Demo(demo.value + another.value)
  }

  def main(args: Array[String]): Unit = {
    val demo: Demo = Demo(1) + Demo(2)
    System.out.println(demo)
  }
}

Third, import the macro-generated Ops$newtype class explicitly -

import io.estatico.newtype.macros.newtype
import Main.Demo.Ops$newtype

object Main {

  @newtype case class Demo(value: Int) {
    def +(another: Demo): Demo = Demo(value + another.value)
  }

  def main(args: Array[String]): Unit = {
    val demo: Demo = Demo(1) + Demo(2)
    System.out.println(demo)
  }
}
carymrobbins commented 4 years ago

I'm going to go ahead and close this, but if there are still unresolved issues, please re-open.