Implicit view not working - is my implicit def to blame?
Asked Answered
D

1

1

I'm having some trouble with an implicit view. I suspect this is quite trivial and may have some embarassingly easy answer. I have a situation like this, along with (obviously, unsuccessful) attempts to debug it:

abstract class StoresNumeric[A, T: Numeric] {
  def getNum(self: A): T
}
object StoresNumericSyntax {
  implicit class StoresNumericOps[A, T: Numeric](value: A) {
    def getNum(implicit sn: StoresNumeric[A, T]): T = sn.getNum(value)
  }
}
case class ANumber[T: Numeric](
  num: T
)
implicit def aNumberStoresNumeric[T: Numeric] = 
  new StoresNumeric[ANumber[T], T] {
    def getNum(self: ANumber[T]): T = self.num
  }
val a = ANumber[Int](3)
// 1. Works fine, so explicit conversion possible
aNumberStoresNumeric[Int].getNum(a) 
// 2. Works fine, so implicit conversion possible
implicitly[StoresNumeric[ANumber[Int], Int]].getNum(a) 
// 3. Doesn't work, so implicit conversion not working
println(implicitly[ANumber[Int] => StoresNumeric[ANumber[Int], Int]]) // no implicit view available...
// 4. The holy grail. Doesn't work, for the same reason as above, plus possibly other
a.getNum

I think the issue here is my implicit def, or more accurately my lack of understanding about implicit def is actually meant to work. I'm not sure what role the generics play - should the first generic argument be used to represent the type I would actually like converted, or is it OK as it is?

Any help gratefully received.

Drumbeat answered 19/9, 2020 at 2:6 Comment(7)
It would be better to use a typeclass instead - also, make your ops class a value class to avoid allocation.Vanderpool
@LuisMiguelMejíaSuárez - thanks - if I understand you correctly, you mean creating a typeclass from a trait rather than an abstract class? If so, this is a simplified example, but I think it does need to be an abstract class, in order to be able to constrain the generic parameters with context bounds. A more detailed example of something similar is here: #63770237Drumbeat
I meant use a typeclass instead of an implicit conversion.Vanderpool
@LuisMiguelMejíaSuárez - apologies but it's not clear to me what you mean - the implicit conversion is there as a means of creating an instance of the StoresNumeric typeclass - do you mean to use another typeclass? I have just tried switching to a value class, but at first glance this only seems to work for concrete typeclasses, ie those with no additional generic parameters.Drumbeat
Ah sorry, I misunderstood your question. You are already using a typeclass. - You only need to import the syntax import StoresNumericSyntax._ before calling a.getNum - Also, try to reformulate your syntax like this: implicit class StoresNumericOps[A](private val value: A) extends AnyVal { and then the method like this: def getNum[T](implicit sn: StoresNumeric[A, T])Vanderpool
@LuisMiguelMejíaSuárez - thanks very much - it seems that switching to the value class method has solved both the example problem and its real-world counterpart. It appears to expand the scope in which the typeclass instance is available? One thing which still silghtly puzzles me is that the implicit conversion (number 3 in my example) still doesn't work - not that it needs to, that was just for debugging. Anyway if you'd like to propose what you said as an answer then I'll accept it.Drumbeat
@Chrisper For me the fix proposed by LuisMiguelMejíaSuárez still doesn't work (without import Numeric.IntIsIntegral) scastie.scala-lang.org/TaMo4kW7QJKX1jzOKVFLkg What version of Scala do you use?Firstclass
F
2

Firstly (#3),

implicitly[ANumber[Int] => StoresNumeric[ANumber[Int], Int]]

is wrong. You do not define an implicit conversion from a data type to a type class, you define an implicit conversion from a data type to an implicit class introducing extension method. So it should be

implicitly[ANumber[Int] => StoresNumericSyntax.StoresNumericOps[ANumber[Int], Int]]

and it compiles.

Be prepared that implicitly[A => B] not always checks that an implicit conversion from A to B exists. * (see below)

Secondly (#4), when you use extension method (a.getNum) you should import syntax object:

import StoresNumericSyntax._
(a: StoresNumericOps[ANumber[Int], Int]).getNum

compiles while

import StoresNumericSyntax._
a.getNum

produces (with scalacOptions += "-Xlog-implicits" switched on)

 Warning:
StoresNumericOps is not a valid implicit value for App.a.type => ?{def getNum: ?} because:
ambiguous implicit values:
 both object BigIntIsIntegral in object Numeric of type scala.math.Numeric.BigIntIsIntegral.type
 and object IntIsIntegral in object Numeric of type scala.math.Numeric.IntIsIntegral.type
 match expected type Numeric[T]

If you import IntIsIntegral you'll add this implicit to the local scope (before that it was in the implicit scope only), so you'll make its "priority" "higher" than the one of BigIntIsIntegral. Try

import StoresNumericSyntax._
import Numeric.IntIsIntegral
a.getNum

It compiles.

Scala 2.13.3.

See also how to debug implicits (at compile time): In scala 2 or 3, is it possible to debug implicit resolution process in runtime?


* For example if you modify the implicit class as @LuisMiguelMejíaSuárez advised in comments

object StoresNumericSyntax {
  implicit class StoresNumericOps[A](private val value: A) extends AnyVal {
    def getNum[T: Numeric](implicit sn: StoresNumeric[A, T]): T = sn.getNum(value)
  }
}

then

import StoresNumericSyntax._
implicitly[ANumber[Int] => StoresNumericOps[ANumber[Int]]]

doesn't compile

 Warning:
StoresNumericOps is not a valid implicit value for ANumber[Int] => StoresNumericSyntax.StoresNumericOps[ANumber[Int]] because:
hasMatchingSymbol reported error: type mismatch;
 found   : StoresNumericSyntax.StoresNumericOps.type
 required: ANumber[Int] => StoresNumericSyntax.StoresNumericOps[App393.ANumber[Int]]

while manually resolved

implicitly[ANumber[Int] => StoresNumericOps[ANumber[Int]]](new StoresNumericOps(_)) 

compiles and

import StoresNumericSyntax._
a: StoresNumericOps[ANumber[Int]]

compiles too.

But if I remove extends AnyVal

object StoresNumericSyntax {
  implicit class StoresNumericOps[A](private val value: A) /*extends AnyVal*/ {
    def getNum[T: Numeric](implicit sn: StoresNumeric[A, T]): T = sn.getNum(value)
  }
}

then

import StoresNumericSyntax._
implicitly[ANumber[Int] => StoresNumericOps[ANumber[Int]]]

compiles.

Also if I split the implicit class into a class + an implicit conversion

object StoresNumericSyntax {
  /*implicit*/ class StoresNumericOps[A](private val value: A) extends AnyVal {
    def getNum[T: Numeric](implicit sn: StoresNumeric[A, T]): T = sn.getNum(value)
  }

  implicit def toStoresNumericOps[A](value: A): StoresNumericOps[A] =
    new StoresNumericOps(value)
}

then

import StoresNumericSyntax._
implicitly[ANumber[Int] => StoresNumericOps[ANumber[Int]]]

compiles.

Why implicitly[A => B] is not the same as val x: B = ??? : A is expained here:

In scala, are there any condition where implicit view won't be able to propagate to other implicit function?

When calling a scala function with compile-time macro, how to failover smoothly when it causes compilation errors?

Scala Kleisli throws an error in IntelliJ

What are the hidden rules regarding the type inference in resolution of implicit conversions?

Scala: `ambigious implicit values` but the right value is not event found

The impact of presence/absence of extends AnyVal on implicit resolution can be a bug but difference between implicit instance (implicitly[A => B]) and implicit conversion (val x: B = ??? : A) is more or less intentional (different strategies of type inference, resolving type parameters are used).

Firstclass answered 19/9, 2020 at 8:0 Comment(3)
Hi Dmytro, thanks as ever for these great answers. I was being a bit hasty yesterday when I said it was all fixed - using a value class fixed my real-world example but not the simplified one above. I'm using 2.13.2 and I get the same results as you - importing IntIsIntegral makes a.getNum work, and using a non-value class for the implicit view and importing StoresNumericSyntax makes the implicitly[A => B] line work. It sounds like you're less of a fan of the extends Anyval approach that Luis mentioned?Drumbeat
I was slightly surprised that it was the ambiguity about implicits that was causing a.getNum not to work - you might think that the compiler could show a more helpful error message than value getNum is not a member of ... since it could presumably tell you that there is a conflict with implicits occurring.Drumbeat
@Chrisper "compiler could show a more helpful error message" But it does provide helpful message as a warning if you switch -Xlog-implicits on. "It sounds like you're less of a fan of the extends Anyval" Value classes are ok, it's an optimization to avoid runtime overhead, although they have many limitations docs.scala-lang.org/overviews/core/value-classes.html (see also github.com/estatico/scala-newtype), they are not supposed to interfere with implicit resolution.Firstclass

© 2022 - 2024 — McMap. All rights reserved.