1

I have a simple GADT declared like this:

sealed trait T[A]
object T {
  case class MkT[A <: String with Singleton](name: A) extends T[A]
}

Now I would like to write a method that will check if the singleton type parameter is the same for two T objects and return evidence of that fact in the form of a cats.evidence.Is object if that is the case. I've tried the following, but it doesn't work:

import cats.evidence.Is
def checkIs[A, B](ta: T[A], tb: T[B]): Option[Is[A, B]] =
  (ta, tb) match {
    case (ta: T.MkT[a], tb: T.MkT[b]) if ta.name == tb.name => 
      Some(Is.refl[A])
    case _ => None
  }
// [error] Main.scala:36:75: type mismatch;
// [error]  found   : cats.evidence.Is[A,A]
// [error]  required: cats.evidence.Is[A,B]

How can I convince the compiler that this is sound?

// edit: as @Dmytro Mitin pointed out, it seems paradoxical to do a run-time check and yet convince the compiler at compile-time that the types are the same. But this is in fact possible, and it can be demonstrated with a simpler GADT:

sealed trait SI[A]
object SI {
  case object S extends SI[String]
  case object I extends SI[Int]
}
def checkInt[A](si: SI[A]): Option[Is[A, Int]] =
  si match {
    case SI.I => Some(Is.refl[Int])
    case _ => None
  }
Matthias Berndt
  • 4,387
  • 1
  • 11
  • 25

1 Answers1

0

With the pattern matching you try to check that "the singleton type parameter is the same for two T objects" at runtime (ta.name == tb.name) but want to convince the compiler at compile time. I would try a type class

trait CheckIs[A, B] {
  def checkIs(ta: T[A], tb: T[B]): Option[Is[A, B]]
}
object CheckIs {
  implicit def same[A]: CheckIs[A, A] = (_, _) => Some(Is.refl[A])
  implicit def diff[A, B]: CheckIs[A, B] = (_, _) => None
}

def checkIs[A, B](ta: T[A], tb: T[B])(implicit ci: CheckIs[A, B]): Option[Is[A, B]] = ci.checkIs(ta, tb)

checkIs(T.MkT("a"), T.MkT("a")) //Some(cats.evidence.Is$$anon$2@28f67ac7)
checkIs(T.MkT("a"), T.MkT("b")) //None

(By the way, Is is a type class, it's natural to use it as implicit constraint but a little weird to use it as return type.)

Dmytro Mitin
  • 48,194
  • 3
  • 28
  • 66
  • This is the magic of GADTs: you do a check at runtime, and yet it convinces the compiler at compile time that two types are the same. I've added an example to demonstrate how this works with a simpler GADT, but I'd like to know how it works with the `T` type above (if that is even possible). A typeclass-based approach doesn't help, because implicit resolution happens purely at compile time. – Matthias Berndt Oct 20 '20 at 08:07
  • @MatthiasBerndt Well, surely Scala has limited support of type inference for GADT. But `ta.name == tb.name` is too runtime thing. And you can't connect type variables `a`, `b`. *"A typeclass-based approach doesn't help, because implicit resolution happens purely at compile time."* Why is that bad? If you're not interested in compile-time solutions you should add this restriction to your question. – Dmytro Mitin Oct 20 '20 at 13:27
  • It's a bad thing because it won't work if `T` is parameterized with a String that is only known at run-time, e. g. read from stdin. I didn't make the requirement explicit because it's kinda obvious – this is what GADTs are for: drawing conclusions on compile time types based on runtime checks. – Matthias Berndt Oct 20 '20 at 14:10
  • @MatthiasBerndt Well, if `name` can be a runtime string then, I'm afraid, what you want generally is not possible. `A <: String with Singleton` doesn't mean that `A` is a singleton type that is a `ConstantType` like types `1`, `"ab"`. It can also be a singleton type that is a `SingleType` like `someVal.type`, `SomeObject.type`. (`A <: String with Singleton` means that compiler should try not to widen `ConstantType`/`SingleType`). You can see that `val s: String = "abc"`, `T.MkT(s)` compiles... – Dmytro Mitin Oct 20 '20 at 15:04
  • @MatthiasBerndt ... Here `A` is inferred `s.type`. But `implicitly[s.type =:= "abc"]` doesn't compile, `SingleType` `s.type` doesn't know anything about `ConstantType` `"abc"`. So if at compile time we can't know types `"abc"`, `"def"` for two runtime strings then at compile time we can't conclude that the types are `=:=`, therefore we can't convince the compiler. Example with GADT `SI` is different. At compile time we know that the pattern `SI.I` has `A =:= Int`. – Dmytro Mitin Oct 20 '20 at 15:05
  • @MatthiasBerndt Surely if you replace `val s: String = "abc"` with `val s: "abc" = "abc"` then `implicitly[s.type =:= "abc"]` compiles and although in `T.MkT(s)` `A` is still inferred `s.type` but in such case `s.type` [knows](https://stackoverflow.com/questions/59567310/how-to-get-underlying-constant-type-from-singleton-type-with-scala-reflection-ap) type `"abc"`. But generally (when `val s: String = "abc"`) `s.type` knows only type `String`. – Dmytro Mitin Oct 20 '20 at 15:19
  • @MatthiasBerndt And when we write `val s: "abc" = "abc"` this means `s` is not a runtime string but a compile-time string literal and in such case type class approach should be ok. – Dmytro Mitin Oct 20 '20 at 15:20
  • @MatthiasBerndt So, summarizing, when you have a runtime string `"abc"` (that is not a compile-time literal) you can't create a value of singleton type `"abc"`, you can only create a value of singleton type `s.type`, which doesn't know about type`"abc"`. – Dmytro Mitin Oct 20 '20 at 15:25