In Scala 2/3, why can't unboxing or view bounds be chained (as in OCaml), and how to fix/circumvent it?

83 Views Asked by At

Considering the following example, derived from the official manual of Scala 3 on Context Abstractions:

https://docs.scala-lang.org/scala3/guides/migration/incompat-contextual-abstractions.html#view-bounds

object ViewBound {

  trait Boxed[T] {
    def v: T
  }

  object Boxed {

    implicit def unbox[T, R](
        boxed: Boxed[T]
    )(
        implicit
        more: T => R
    ): R = more(boxed.v)
  }

  case class B0(v: String) extends Boxed[String]
  case class B1(v: B0) extends Boxed[B0]
  case class B2(v: B1) extends Boxed[B1]

  val b2 = B2(B1(B0("a")))

  val ab = b2.concat("b")
}

Theoretically, the view bound unbox should be used repeatedly until the function concat is found, in each iteration unbox is a valid candidate in scope. In practice, the compiler will report the following error (tested in Scala 3.4.2-snapshot build):

[Error] /home/peng/git/dottyspike/src/main/scala/com/tribbloids/spike/dotty/ViewBound.scala:25:15: value concat is not a member of com.tribbloids.spike.dotty.ViewBound.B2.
An extension method was tried, but could not be fully constructed:

    com.tribbloids.spike.dotty.ViewBound.Boxed.unbox[
      com.tribbloids.spike.dotty.ViewBound.B1,
      com.tribbloids.spike.dotty.ViewBound.B1](
      com.tribbloids.spike.dotty.ViewBound.b2)(
      $conforms[com.tribbloids.spike.dotty.ViewBound.B1])

What's the cause of this behaviour and how to make it work?

UPDATE 1: just tried the typeclass idea and Nope, still doesn't work:


  object V2 {

    trait Boxed[T] {
      def self: T
    }

    object Boxed {

      trait ViewBound[S, R] extends (S => R) {}

      implicit def unbox1[T]: ViewBound[Boxed[T], T] = v => v.self

      implicit def unboxMore[T, R](
          implicit
          more: ViewBound[T, R]
      ): ViewBound[Boxed[T], R] = v => more(v.self)

      implicit def convert[T, R](v: Boxed[T])(
          implicit
          unbox: ViewBound[Boxed[T], R]
      ): R = unbox(v)
    }

    case class B0(self: String) extends Boxed[String]
    val b0 = B0("a")
    b0.concat("b")

    case class B1(self: B0) extends Boxed[B0]
    val b1 = B1(B0("a"))
    b1.concat("b")

    case class B2(self: B1) extends Boxed[B1]
    val b2 = B2(B1(B0("a")))
    b2.concat("b")
  }
[Error] /home/peng/git/dottyspike/src/main/scala/com/tribbloids/spike/dotty/ViewBound.scala:27:17: value concat is not a member of com.tribbloids.spike.dotty.ViewBound.Failed.B2.
An extension method was tried, but could not be fully constructed:

    com.tribbloids.spike.dotty.ViewBound.Failed.Boxed.unbox[
      com.tribbloids.spike.dotty.ViewBound.Failed.B1,
      com.tribbloids.spike.dotty.ViewBound.Failed.B1](
      com.tribbloids.spike.dotty.ViewBound.Failed.b2)(
      $conforms[com.tribbloids.spike.dotty.ViewBound.Failed.B1])
[Error] /home/peng/git/dottyspike/src/main/scala/com/tribbloids/spike/dotty/ViewBound.scala:59:8: value concat is not a member of com.tribbloids.spike.dotty.ViewBound.Success.B1.
An extension method was tried, but could not be fully constructed:

    com.tribbloids.spike.dotty.ViewBound.Success.Boxed.convert[
      com.tribbloids.spike.dotty.ViewBound.Success.B0,
      com.tribbloids.spike.dotty.ViewBound.Success.B0](
      com.tribbloids.spike.dotty.ViewBound.Success.b1)(
      com.tribbloids.spike.dotty.ViewBound.Success.Boxed.unbox1[
        com.tribbloids.spike.dotty.ViewBound.Success.B0]
    )
1

There are 1 best solutions below

1
Dmytro Mitin On

You could use a type class + extension method rather than implicit conversions

trait Unbox[-A, +B] {
  def apply(a: A): B
}
object Unbox {
  implicit def recurse[A, B](implicit u: Unbox[A, B]): Unbox[Boxed[A], B] = boxed => u(boxed.v)
  implicit def base[A]: Unbox[Boxed[A], A] = _.v
}

implicit class BoxedOps[T](val boxed: T) extends AnyVal {
  def concat(s: String)(implicit u: Unbox[T, String]): String = u(boxed).concat(s)
}
case class B0(v: String) extends Boxed[String]
case class B1(v: B0) extends Boxed[B0]
case class B2(v: B1) extends Boxed[B1]

val b0 = B0("a")
val b1 = B1(b0)
val b2 = B2(b1)

b0.concat("b") //ab
b1.concat("b") //ab
b2.concat("b") //ab

It can be dangerous to formulate your logic in terms of implicit conversions. They are tricky. They are resolved/typechecked differently than implicits of non-functional types.

The problem with inductive definitions

implicit def foo[A, B]...(implicit x: X[A, B])...

is to decide what to prefer: to define the implicit for arbitrary A, B (i.e. to postpone type inference, lazy strategy) or to infer e.g. B based on A (i.e. to make type inference now, eager strategy). For implicit conversions (isView == true) the eager strategy is preferred, for all other non-functional types (isView == false) the lazy one is preferred

https://github.com/scala/scala/blob/232521f418c1e2c535d3630b0a5b3972a06bbd4e/src/compiler/scala/tools/nsc/typechecker/Implicits.scala#L846-L866

val itree2 = if (!isView) fallback else pt match {
  case Function1(arg1, arg2) =>
    typed1(
      atPos(itree0.pos)(Apply(itree1, Ident(nme.argument).setType(approximate(arg1)) :: Nil)),
      EXPRmode,
      approximate(arg2)
    ) match {
      // try to infer implicit parameters immediately in order to:
      //   1) guide type inference for implicit views
      //   2) discard ineligible views right away instead of risking spurious ambiguous implicits
      //
      // this is an improvement of the state of the art that brings consistency to implicit resolution rules
      // (and also helps fundep materialization to be applicable to implicit views)
      //
      // there's one caveat though. we need to turn this behavior off for scaladoc
      // because scaladoc usually doesn't know the entire story
      // and is just interested in views that are potentially applicable
      // for instance, if we have `class C[T]` and `implicit def conv[T: Numeric](c: C[T]) = ???`
      // then Scaladoc will give us something of type `C[T]`, and it would like to know
      // that `conv` is potentially available under such and such conditions
      case tree if isImplicitMethodType(tree.tpe) && !isScaladoc =>
        applyImplicitArgs(tree)

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

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

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

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

https://contributors.scala-lang.org/t/can-we-wean-scala-off-implicit-conversions/4388

https://contributors.scala-lang.org/t/proposed-changes-and-restrictions-for-implicit-conversions/4923

How can I chain implicits in Scala?