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

0

3

I thought that if the following compiles:

implicitly[X => Y]

than so will this:

(??? :X) :Y

It turns out I was wrong. Backstory: I toyed with an implementation of type unions:

private[this] val cast = identity[Any] _

abstract class TypeUnionLevel4Implicits {
    implicit def implicitUnionUnification[L, R, U <: Any | Any](implicit left :L => U, right :R => U) :(L | R) => U =
        left.asInstanceOf[(L | R) => U]
}

sealed abstract class TypeUnionLevel3Implicits extends TypeUnionLevel4Implicits {
    implicit def implicitRightComposedUnionMember[X, L, R <: Any | Any](implicit right :X => R) :X => (L | R) =
        right.asInstanceOf[X => (L | R)]
}

sealed abstract class TypeUnionLevel2Implicits extends TypeUnionLevel3Implicits {
    implicit def implicitLeftComposedUnionMember[X, L <: Any | Any, R](implicit left :X => L) :X => (L | R) =
        left.asInstanceOf[X => (L | R)]
}

sealed abstract class TypeUnionLevel1Implicits extends TypeUnionLevel2Implicits {
    implicit def implicitRightUnionMember[L, R] :R => (L | R) = cast.asInstanceOf[R => (L | R)]
}

abstract class TypeUnionImplicits private[slang] extends TypeUnionLevel1Implicits {
    implicit def implicitLeftUnionMember[L, R] :L => (L | R) = cast.asInstanceOf[L => (L | R)]
}

object union extends TypeUnionImplicits {
    type |[+L, +R]
}

Testing:

implicitly[String => (String | Int)]
implicitly[String => (Int | String)]

implicitly[(String | Int) => (Int | String)]
implicitly[String => (Int | String | Double)]
implicitly[String => (String | Int | Double | Short)]
implicitly[String => (Short | (Double | (Int | String)))]
implicitly[(String | Int | Double | Short) => (Short | Double | Int | String)]

Compiles! Success! Or is it?

val left = "left" :String | Int
val right = "right" :Int | String

val swap = (left :String | Int) :Int | String
val middle = "middle" :Int | String | Double
val farLeft = "farLeft" :String | Int | Double | Short
val farRight = "farRight" :Short | (Double | (Int | String))
val shuffle = farLeft :Short | Double | Int | String

compile...compile...compile...

Information:(14, 19) typist.this.`package`.implicitUnionUnification is not a valid implicit value for String | Int => Int | String because:
not enough arguments for method implicitUnionUnification: (implicit left: L => U, right: R => U): L | R => U.
Unspecified value parameter right.
    val swap = (left :String | Int) :Int | String  
Error:(14, 19) type mismatch;
found   : String | Int
required: Int | String
    val swap = (left :String | Int) :Int | String

I opened an issue at /dev/null because it surely must be a bug. But it looks just so plain and basic stuff, that it seems there must be a way to work around it. What I tried:

  • direct implicit conversions as normal methods, not function-returning ones;

  • an intermediate implicit class

       class TypeUnionMember[X, U]
    

    with the implicit methods above returning it instead of X=>U, together with one top level implicit providing X=>U when TypeUnionMember[X, U] implicit exists:

       implicit def widenToTypeUnion[X, U](implicit unify :TypeUnionMember[X, U]) :X => U = cast.asInstanceOf[X, U]
    

The latter proved interesting and by interesting I mean frustrating: the logs sad that widenToTypeUnion is an invalid implicit for String => String | Int because an implicit TypeUnionMember[String, Nothing] cannot be found. What?

Laveralavergne answered 20/7, 2020 at 19:21 Comment(14)
I thought that if the following compiles: implicitly[X => Y] than so will this: (??? :X) :Y Nope. #62630939 #62206440 #62751993Transference
contributors.scala-lang.org/t/…Transference
Thanks yet again. This time however I already did exactly what you recomended in the first link: a special type class TypeUnionMember with only a single implicit conversion declaration based on it, as per the last snippet, and it didn't change anything at all. Or did you mean for type classes to completely replace the conversion, as in no way of automatically converting from one type to another in arbitrary places?Laveralavergne
I read the post by M.Odersky some time ago and repressed it, as the fact that I keep playing all the time with features marked for removal fills me with dread. My only hope is that there indeed will be 'better ways to do things' and more robust type unification.Laveralavergne
If, instead of functions, you return some other type (like <:<), and then define a single function that uses that evidence parameter (A <:< B) to turn an A into a B, it seems to work. Here's an implementation that might suit your needs after a little polishingWain
@user Variance of Is is incorrect. It should be Is[-A, +B]. With wrong variance some things compile while they shouldn't. For example implicitly[Any Is (String | Int)], implicitly[Any Is Nothing], implicitly[(Boolean | Double) Is (String | Int)].Transference
@Laveralavergne The issue is with type inference. Suppose a typeclass has any instances trait TC[A, B] implicit def mkTC[A, B]: TC[A, B] = null then an instance is found implicitly[TC[Int, String]] but implicit conversion implicit def conversion[A, B](a: A)(implicit tc: TC[A, B]): B = ??? will not work val s: String = 1 // error. Debug logs show incompatible: (a: Int)(implicit tc: TC[Int,B]): B does not match expected type Int(1) => String so B is not inferred.Transference
@DmytroMitin Oops. It doesn't work at all anymore (Scastie). Thanks for pointing that outWain
I think your implicit conversion should look like implicit def widenToTypeUnion[X, U](x: X)(implicit unify: TypeUnionMember[X, U]): U instead.Unvoiced
@Unvoiced This doesn't change the behavior.Transference
@Laveralavergne How about something like this, where there are implicit defs but the conversion is explicit? I haven't tested it much yet, but you might be able to do stuff with it. However, I'd really just recommend upgrading to Dotty if you need actual union typesWain
Thank you all for weighting in. As I said, fortunately this is something I only toyed with and don't particularly need in the moment. I wanted to understand why it doesn't work, especially that it looked like a bug (and, of course, fix it if possible). I am aware that the explicit conversion with an implicit evidence will work, but it detracts considerably from the elegance - I believe it is a good example where implicit conversions indeed shine.Laveralavergne
@Laveralavergne I played with that. github.com/DmytroMitin/scala/commits/implicit-conversions I fixed inference in the code in my above comment. And I guess that fixed compilation of your code rewritten with a type class + conversion. But unfortunately that broke many other tests of compiler inference for implicit conversions. Maybe I'll give it a try later.Transference
@Laveralavergne Currently draft PR is here github.com/scala/scala/pull/9148 but probably will be deleted soon although the branch in my fork remains.Transference

© 2022 - 2024 — McMap. All rights reserved.