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?

Rizwan
  • 103
  • 4
  • 24
Turin
  • 2,208
  • 15
  • 23
  • 4
    *I thought that if the following compiles: `implicitly[X => Y]` than so will this: `(??? :X) :Y`* Nope. https://stackoverflow.com/questions/62630439/in-scala-are-there-any-condition-where-implicit-view-wont-be-able-to-propagate https://stackoverflow.com/questions/62205940/when-calling-a-scala-function-with-compile-time-macro-how-to-failover-smoothly https://stackoverflow.com/questions/62751493/scala-kleisli-throws-an-error-in-intellij – Dmytro Mitin Jul 20 '20 at 19:37
  • 2
    https://contributors.scala-lang.org/t/can-we-wean-scala-off-implicit-conversions/4388 – Dmytro Mitin Jul 20 '20 at 19:51
  • 1
    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? – Turin Jul 20 '20 at 21:44
  • 2
    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. – Turin Jul 20 '20 at 21:46
  • 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](https://scastie.scala-lang.org/kPGFQpxORDC5Gl6WfgQu1A) an implementation that might suit your needs after a little polishing – user Jul 21 '20 at 15:44
  • 1
    @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)]`. – Dmytro Mitin Jul 21 '20 at 18:52
  • 1
    @Turin 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. – Dmytro Mitin Jul 21 '20 at 19:10
  • @DmytroMitin Oops. It doesn't work at all anymore ([Scastie](https://scastie.scala-lang.org/piCaybARRiKZmMd5iFJT2g)). Thanks for pointing that out – user Jul 21 '20 at 19:12
  • I think your implicit conversion should look like `implicit def widenToTypeUnion[X, U](x: X)(implicit unify: TypeUnionMember[X, U]): U` instead. – Jasper-M Jul 21 '20 at 19:35
  • 1
    @Jasper-M This doesn't change the behavior. – Dmytro Mitin Jul 21 '20 at 19:56
  • @Turin How about something like [this](https://scastie.scala-lang.org/mKO9fOtvRMW436YnTLfXug), 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 types – user Jul 22 '20 at 20:21
  • 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. – Turin Jul 23 '20 at 14:34
  • @Turin I played with that. https://github.com/DmytroMitin/scala/commits/implicit-conversions I fixed inference in the code in my above [comment](https://stackoverflow.com/questions/63002466/what-are-the-hidden-rules-regarding-the-type-inference-in-resolution-of-implicit#comment111445706_63002466). 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. – Dmytro Mitin Sep 24 '20 at 22:42
  • @Turin Currently draft PR is here https://github.com/scala/scala/pull/9148 but probably will be deleted soon although the branch in my fork remains. – Dmytro Mitin Sep 24 '20 at 23:06

0 Answers0