Scala implicit def do not work if the def name is toString
Asked Answered
P

2

4

This code fails to compile:

object Foo {
  implicit def toString(i: Int): String = i.toString      
  def foo(x: String) = println(x)
  foo(23)
}    

Above code fails to compile with following error:

error: type mismatch;
found   : scala.this.Int(23)
required: String
  foo(23)

But, this code compiles

object Foo {
  implicit def asString(i: Int): String = i.toString      
  def foo(x: String) = println(x)
  foo(23)
}

Why does the name of an implicit def should matter?

Note: If the method is named equals, it also does not work but it works if it is named hashCode or clone etc.

Preset answered 25/2, 2019 at 17:54 Comment(2)
What's the error?Annoyance
found: Int; required: StringPreset
W
12

The problem here isn't that toString is overloaded on Foo, as one of the other (now-deleted) answers argues (you can try overloading asString similarly and it'll work), it's that the toString you're importing collides with the toString of the enclosing class (in your case some synthetic object made up by the REPL).

I think the following implicit-free examples (which also don't use "built-in" method names like toString) show the issue a little more clearly:

class Foo {
  def asString(i: Int): String = "this is the one from Foo!"
}

class Bar {
  def asString(i: Int): String = "this is the one from Bar!"
}

object Demo extends Bar {
  val instance = new Foo
  import instance._

  println(asString(23))
}

This will use the asString from Bar, even though you might think the imported one would take precedence:

scala> Demo
this is the one from Bar!
res1: Demo.type = Demo$@6987a133

In fact it'll use the definition from Bar even if the arguments don't line up:

class Foo {
  def asString(i: Int): String = "this is the one from Foo!"
}

class Bar {
  def asString(): String = "this is the one from Bar!"
}

object Demo extends Bar {
  val instance = new Foo
  import instance._

  println(asString(23))
}

This fails to compile:

<pastie>:25: error: no arguments allowed for nullary method asString: ()String
  println(asString(324))
                   ^

Now we can make this look more like your original code:

class Foo {
  implicit def asString(i: Int): String = "this is the one from Foo!"
  def foo(s: String): String = s
}

class Bar {
  def asString(): String = "this is the one from Bar!"
}

object Demo extends Bar {
  val instance = new Foo
  import instance._

  println(foo(23))
}

This fails with the same error you saw, for the same reason: the imported implicit conversion is hidden by the definition with the same name in the enclosing class.

Footnote 1

You asked the following:

Why does the name of an implicit def should matter?

Names of implicits matter all the time. That's just the way the language works. For example:

scala> List(1, 2, 3) + ""
res0: String = List(1, 2, 3)

scala> trait Garbage
defined trait Garbage

scala> implicit val any2stringadd: Garbage = new Garbage {}
any2stringadd: Garbage = $anon$1@5b000fe6

scala> List(1, 2, 3) + ""
<console>:13: error: value + is not a member of List[Int]
       List(1, 2, 3) + ""
                     ^

What we've done is defined an implicit value that hides the any2stringadd implicit conversion in scala.Predef. (Yes, this is kind of terrifying.)

Footnote 2

I think there's probably a compiler bug here, at least as far as the error message is concerned. If you change things up just a little bit in my second version above, for example:

class Foo {
  def asString(i: Int): String = "this is the one from Foo!"
}

class Bar {
  def asString(): String = "this is the one from Bar!"
}

object Demo extends Bar {
  def test(): Unit = {
    val instance = new Foo
    import instance._

    println(asString(23))
  }
}

…you get a much more reasonable message:

<pastie>:26: error: reference to asString is ambiguous;
it is both defined in class Bar and imported subsequently by
import instance._
    println(asString(23))
            ^

In my view this is almost certainly the kind of thing the compiler should tell you in your original case. I'm also not sure why the hidden implicit is considered for the conversion at all, but it is, as you can tell if you run your code in a REPL with -Xlog-implicits:

scala> foo(23)
<console>:16: toString is not a valid implicit value for Int(23) => String because:
no arguments allowed for nullary method toString: ()String
       foo(23)
           ^

So it looks like the implicitness is rubbing off on the other toString? To be honest I have no idea what's happening here, but I'm like 90% sure it's a mistake.

Wessex answered 25/2, 2019 at 18:42 Comment(0)
D
2

Not sure if this counts as an answer (probably someone with more knowledge on the internals of the compiler could give a more detailed explanation), but after playing a while with your code I found something, which I believe is the root of the error.

Given:

object Foo {
  implicit def toString(i: Int): String = i.toString      
}

import Foo.toString

Then:

val s: String = 10

Produces:

:10: warning: imported `toString' is permanently hidden by definition of method toString in class Object
import Foo.toString

Which I think means that, the implicit conversion is being hidden because its name collides with the universal toString method defined in java.langObject (and scala.Any).


Curios enough, this works.

implicit val int2str: Int => String = Foo.toString
val s: String = 10
// s: String = 10
Dawn answered 25/2, 2019 at 18:11 Comment(3)
But you changed the original code ... that error makes sense if you are importing import Foo.toString. Possibly related and in the right direction but not the actual answer - my overloading of toString is completely legal (different number of params)Preset
@Preset As I said, I do know this is not the answer, but wanted to share what I found (I would made it a comment if possible). However, I do not think the change of the import affects, since there is no way to choose which one of the two toString methods one wants to import. Also, Travis comment showed that the problem may not be directly related to the overloading of a method, but rather (probably) with something internal to the compiler related with the toString method.Dor
I tried other names which are special for compiler (e.g. equals) and it fails to compile while others (e.g. hashcode) compiles.Preset

© 2022 - 2024 — McMap. All rights reserved.