Skip to content

Implicit conversions for unions #4806

@steinybot

Description

@steinybot

Has there been any consideration for getting implicit conversions to work with unions?

I've come up with this which seems to work according to my (limited) testing:

import shapeless.<:!<

import scala.annotation.unused
import scala.reflect.ClassTag
import scala.scalajs.js
import scala.scalajs.js.|
import scala.scalajs.js.|.Evidence

trait UnionOps extends HighPriorityUnionOps {

  // These were copied from js.|.
  // They have to be here since this is explicitly imported and it will be searched before the compiler goes and checks
  // the js.| companion object which ends up taking about 4 times as long. I am not entirely sure why having them here
  // is actually faster since I would expect it to find the same candidates before ranking and ultimately picking these.
  // Perhaps there are some smarts where it does not consider candidates that could not possibly get a higher ranking.

  /** Upcast `A` to `B1 | B2`.
    *
    * This needs evidence that `A <: B1 | B2`.
    */
  implicit def from[A, B1, B2](a: A)(implicit @unused ev: Evidence[A, B1 | B2]): B1 | B2 =
    a.asInstanceOf[B1 | B2]

  /** Upcast `F[A]` to `F[B]`.
    *
    * This needs evidence that `F[A] <: F[B]`.
    */
  implicit def fromTypeConstructor[F[_], A, B](a: F[A])(implicit @unused ev: Evidence[F[A], F[B]]): F[B] =
    a.asInstanceOf[F[B]]

}

trait NoEvidence[A, B]

object NoEvidence {
  // If Evidence is found then these will be ambiguous otherwise noEvidence will be found.
  implicit def noEvidence[A, B]: NoEvidence[A, B]                                       = new NoEvidence[A, B] {}
  implicit def hasEvidence[A, B](implicit @unused ev: Evidence[A, B]): NoEvidence[A, B] = new NoEvidence[A, B] {}
}

trait MappableUnion[A] {
  // TODO: What to call these?
  def mapLeft[A2, B1, B2](a: A | A2)(a1: A => B1, a2: A2 => B2): B1 | B2
  def mapRight[A1, B1, B2](a: A1 | A)(a1: A1 => B1, a2: A => B2): B1 | B2
}

object MappableUnion {

  implicit def fromClassTag[A: ClassTag](implicit
    // No isInstance of allowed on js.Any.
    @unused ev1: A <:!< js.Any,
    // isInstance of on js.| blows up at runtime too so make sure that is not allowed.
    @unused ev2: A <:!< |[_, _]
  ): MappableUnion[A] = new MappableUnion[A] {

    override def mapLeft[A2, B1, B2](a: A | A2)(f1: A => B1, f2: A2 => B2): B1 | B2 = (a: Any) match {
      case a1: A => f1(a1)
      case a     => f2(a.asInstanceOf[A2])
    }

    override def mapRight[A1, B1, B2](a: A1 | A)(f1: A1 => B1, f2: A => B2): B1 | B2 = (a: Any) match {
      case a2: A => f2(a2)
      case a     => f1(a.asInstanceOf[A1])
    }
  }
}

trait HighPriorityUnionOps extends LowPriorityUnionOps {

  implicit def toLeft[A, B1, B2](a: A)(implicit @unused noEv: NoEvidence[A, B1], conv: A => B1): B1 | B2 =
    conv(a)

  implicit def toRight[A, B1, B2](a: A)(implicit @unused noEv: NoEvidence[A, B2], conv: A => B2): B1 | B2 =
    conv(a)

  implicit def convertLeft[A1, A2, B1](a: A1 | A2)(implicit
    // This ensures that fromTypeConstructor wins.
    @unused noEv: NoEvidence[A1 | A2, B1 | A2],
    mapper: MappableUnion[A1],
    conv: A1 => B1
  ): B1 | A2 =
    mapper.mapLeft(a)(conv, identity)

  implicit def convertRight[A1, A2, B2](a: A1 | A2)(implicit
    // This ensures that fromTypeConstructor wins.
    @unused noEv: NoEvidence[A1 | A2, A1 | B2],
    mapper: MappableUnion[A1],
    conv: A2 => B2
  ): A1 | B2 =
    mapper.mapLeft(a)(identity, conv)
}

trait LowPriorityUnionOps extends LowerPriorityUnionOps {

  // These are lower priority otherwise they are ambiguous when both A1 and A2 are a MappableUnion.

  implicit def convertLeft2[A1, A2, B1](a: A1 | A2)(implicit
    // This ensures that fromTypeConstructor wins.
    @unused noEv: NoEvidence[A1 | A2, B1 | A2],
    mapper: MappableUnion[A2],
    conv: A1 => B1
  ): B1 | A2 =
    mapper.mapRight(a)(conv, identity)

  implicit def convertRight2[A1, A2, B2](a: A1 | A2)(implicit
    // This ensures that fromTypeConstructor wins.
    @unused noEv: NoEvidence[A1 | A2, A1 | B2],
    mapper: MappableUnion[A2],
    conv: A2 => B2
  ): A1 | B2 =
    mapper.mapRight(a)(identity, conv)
}

trait LowerPriorityUnionOps extends LowestPriorityUnionOps {

  implicit def convertBoth[A1, A2, B1, B2](a: A1 | A2)(implicit
    // This ensures that fromTypeConstructor wins.
    @unused noEv: NoEvidence[A1 | A2, B1 | B2],
    mapper: MappableUnion[A1],
    conv1: A1 => B1,
    conv2: A2 => B2
  ): B1 | B2 =
    mapper.mapLeft(a)(conv1, conv2)
}

trait LowestPriorityUnionOps {

  // This is lower priority otherwise it is ambiguous when both A1 and A2 are a MappableUnion.

  implicit def convertBoth2[A1, A2, B1, B2](a: A1 | A2)(implicit
    // This ensures that fromTypeConstructor wins.
    @unused noEv: NoEvidence[A1 | A2, B1 | B2],
    mapper: MappableUnion[A2],
    conv1: A1 => B1,
    conv2: A2 => B2
  ): B1 | B2 =
    mapper.mapRight(a)(conv1, conv2)
}

The compile times seem to be ok so far.

Metadata

Metadata

Assignees

No one assigned

    Labels

    as-designedThe observed behavior is as-designed, it need not be fixed.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions