1

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]

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
Nigel Benns
  • 1,236
  • 11
  • 14
  • Does it really make sense to upcast both warnings and errors into the same type? How do you differentiate them latter? Why not just using something like `List[Either[Warning, Error]]`? or your own `WarningOrError` **ADT**? – Luis Miguel Mejía Suárez Aug 31 '20 at 14:16
  • that's a thought actually. I may do that. I would still like to know if there is an answer to how to do it though, as it has become more of a curiosity to me :) – Nigel Benns Aug 31 '20 at 14:43
  • 1
    @NigelBenns Please prepare [MCVE](https://stackoverflow.com/help/minimal-reproducible-example) (you didn't provide definitions of `Validator`, `Result` etc.). Also please specify what's wrong with 3 code snippets you wrote (how do you apply `or`, how did you check that *"Scala resolves the `W2` as `Any`"*). *"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`"* Would you please make it clear what you are trying to achieve? `Any` is a supertype of arbitrary `W` and `E`. What is "**the** supertype"? – Dmytro Mitin Aug 31 '20 at 18:52
  • 1
    Question updated with a usecase. If need be I can try to simplify things down to just those types and provide a full working example. – Nigel Benns Sep 01 '20 at 19:11

1 Answers1

3

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 }
Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • 1
    I had to use Shapless' Lub instead of the one there, it was still giving me `Any`. However once switching to that it worked fine. Thanks! – Nigel Benns Sep 03 '20 at 04:22
  • @NigelBenns Well, honestly I forgot that there was `shapeless.Lub` :) Actually, for me `shapeless.Lub` seems to work https://scastie.scala-lang.org/F0pWNaYSStyTEwWMKqHLpA – Dmytro Mitin Sep 03 '20 at 07:27