Search code examples
scalatypeclasscovarianceunion-types

Covariance between 3 types in Scala


I'm trying to see if there is a way to find a type W2 that is the super type of 2 types W and E. In my solution E represents errors, and W represents Warnings. What I'm trying to accomplish is a method or that if this fails runs that and moves the error to the warning type.

Here is a simplified example of what I'm doing.

sealed trait Validator[-I, +E, +W, +A] extends Product with Serializable

this type has several cases, which aren't really important here, so instead I'll go over an example usage:

case class MyObj(coords: GeoCoords)
case class GeoCoords(lat: String, long: String)
        
// Getters
val getLatitude: Validator[GeoCoords, Nothing, Nothing, String] = from[GeoCoords].map(_.lat)
val getLongitude: Validator[GeoCoords, Nothing, Nothing, String] = from[GeoCoords].map(_.long)

val parseLatitude: Validator[GeoCoords, Exception, Nothing, Double] = getLatitude andThen nonEmptyString andThen convertToDouble
val parseLongitude: Validator[GeoCoords, Exception, Nothing, Double] = getLongitude andThen convertToDouble

Now this is a bad example, but what I'm looking to do is that because parseLatitude has an error type of Exception, perhaps I want to give a default instead, but still understand that it failed. I'd like to move the Exception from the E error param to the W warning param like this:

val defaultLatitude: Validator[GeoCoords, Nothing, Nothing, Double] = success(0)

val finalLatitude: Validator[GeoCoords, Nothing, Exception, Double] = parseLatitude or defaultLatitude

But as well, if instead of supplying default, the other action I take after the or may fail as well, therefore this should also be the case:

val otherAction: Validator[GeoCoords, Throwable, Nothing, Double] = ???

val finalLatitude: Validator[GeoCoords, Throwable, Exception, Double] = parseLatitude or otherAction

I've tried implementing or in several ways on the Validator type but everytime it gives me an issue, basically casting things all the way up to Any.

def or[I2 <: I, E2 >: E, W2 >: W, B >: A](that: Validator[I2, E2, W2, B]): Validator[I2, E2, W2, B] = Validator.conversion { (i: I2) =>
  val aVal: Validator[Any, E, W, A] = this.run(i)
  val bVal: Validator[Any, E2, W2, B] = that.run(i)

  val a: Vector[W2] = aVal.warnings ++ bVal.warnings
  // PROBLEM HERE
  val b: Vector[Any] = a ++ aVal.errors

  Result(
    aVal.warnings ++ aVal.errors ++ bVal.warnings,
    bVal.value.toRight(bVal.errors)
  )
}

I need to be able to say that W2 is both a supertype of W and of E so that I can concatenate the Vectors together and get type W2 at the end.

A super simplified self contained example is:

case class Example[+A, +B](a: List[A], b: List[B]) {
  def or[A2 >: A, B2 >: B](that: Example[A2, B2]): Example[A2, Any] = {
    Example(that.a, this.a ++ this.b ++ that.a)
  }
}

object stackoverflow extends App {

  val example1 = Example(List(1, 2), List(3, 4))
  val example2 = Example(List(5, 6), List(7, 8))

  val example3 = example1 or example2

}

Where I want the output type of or to be Example[A2, B2] instead of Example[A2, Any]


Solution

  • Actually you want the concatenation of a List[A] and a List[B] to produce a List[A | B], where A | B is union type.

    How to define "type disjunction" (union types)?

    In Scala 2 union types are absent but we can emulate them with type classes.

    So regarding "super simplified example" try a type class LUB (least upper bound)

    case class Example[A, B](a: List[A], b: List[B]) {
      def or[A2 >: A, B2, AB](that: Example[A2, B2])(
        implicit
        lub: LUB.Aux[A, B, AB],
        lub1: LUB[AB, A2]
      ): Example[A2, lub1.Out] = {
        Example(that.a, (this.a.map(lub.coerce1) ++ this.b.map(lub.coerce2)).map(lub1.coerce1(_)) ++ that.a.map(lub1.coerce2(_)))
      }
    }
    
    val example1: Example[Int, Int] = Example(List(1, 2), List(3, 4))
    val example2: Example[Int, Int] = Example(List(5, 6), List(7, 8))
    
    val example3 = example1 or example2
    //  example3: Example[Int, Any] // doesn't compile
    example3: Example[Int, Int] // compiles
    
    trait LUB[A, B] {
      type Out
      def coerce1(a: A): Out
      def coerce2(b: B): Out
    }
    
    trait LowPriorityLUB {
      type Aux[A, B, Out0] = LUB[A, B] { type Out = Out0 }
      def instance[A, B, Out0](f: (A => Out0, B => Out0)): Aux[A, B, Out0] = new LUB[A, B] {
        override type Out = Out0
        override def coerce1(a: A): Out0 = f._1(a)
        override def coerce2(b: B): Out0 = f._2(b)
      }
    
      implicit def bSubtypeA[A, B <: A]: Aux[A, B, A] = instance(identity, identity)
    }
    
    object LUB extends LowPriorityLUB {
      implicit def aSubtypeB[A <: B, B]: Aux[A, B, B] = instance(identity, identity)
      implicit def default[A, B](implicit ev: A <:!< B, ev1: B <:!< A): Aux[A, B, Any] = 
        instance(identity, identity)
    }
    
    // Testing:
    implicitly[LUB.Aux[Int, AnyVal, AnyVal]]
    implicitly[LUB.Aux[AnyVal, Int, AnyVal]]
    implicitly[LUB.Aux[Int, String, Any]]
    implicitly[LUB.Aux[Int, Int, Int]]
    //  implicitly[LUB.Aux[Int, AnyVal, Any]] // doesn't compile
    

    <:!< is from here:

    Scala: Enforcing A is not a subtype of B

    https://github.com/milessabin/shapeless/blob/master/core/src/main/scala/shapeless/package.scala#L48-L52

    If you want Example to be covariant

    case class Example[+A, +B]...
    

    make LUB and LUB.Aux contravariant

    trait LUB[-A, -B]...
    
    type Aux[-A, -B, Out0] = LUB[A, B] { type Out = Out0 }