如何在元组上使用Scala 3递归匹配类型

xt0899hw  于 2023-05-22  发布在  Scala
关注(0)|答案(1)|浏览(131)

我试图建立一个小的库来表示类型化路径。下面我有一个简化的版本,试图表达我想要的概念。
有没有办法让make不发出错误?

package bar

object XX {

  @FunctionalInterface
  trait SegmentDecoder[A] {
    self =>
    def decode(segment: String): Either[String, A]

    final def map[B](f: A => B): SegmentDecoder[B] = (s) => self.decode(s).map(f)

    final def flatMap[B](f: A => SegmentDecoder[B]): SegmentDecoder[B] = (s) => self.decode(s).flatMap(a => f(a).decode(s))
  }

  object SegmentDecoder {
    given SegmentDecoder[Int] = (seg) => seg.toIntOption.toRight(s"'$seg' is not a valid int")

    given SegmentDecoder[Long] = (seg) => seg.toLongOption.toRight(s"'$seg' is not a valid long")

    given SegmentDecoder[String] = (seg) => Right(seg)

    given SegmentDecoder[Unit] = (_) => Right(())
  }

  case class Param[A](name: String, converter: SegmentDecoder[A])

  def param[A](name: String)(using S: SegmentDecoder[A]) = Param(name, S)

  enum P[A <: Tuple] {
    def /(segment: String) = Static(this, segment)

    def /[A](param: Param[A]) = Variable(this, param.name, param.converter)

    case Root extends P[EmptyTuple]
    case Static[T <: Tuple](parent: P[T], name: String) extends P[T]
    case Variable[H, T <: Tuple](parent: P[T], name: String, decoder: SegmentDecoder[H]) extends P[Tuple.Append[T, H]]
  }

  type Elem[X <: Tuple] = X match {
    case EmptyTuple => P.Root.type
    case String *: xs => P.Static[xs]
    case Param[y] *: xs => P.Variable[y, xs]
  }

  def makeBroken[T <: Tuple](t: T): Elem[T] = {
    t match {
      case _: EmptyTuple => P.Root
      case (x *: xx): (String *: xs) => P.Static(makeBroken[xs](xx).asInstanceOf[P[xs]], x)
      case (x *: xx): (Param[t] *: xs) => P.Variable(makeBroken[xs](xx).asInstanceOf[P[xs]], x.name, x.converter)
    }
  }

  def make[T <: Tuple](t: T): P[T] = {
    t match {
      case _: EmptyTuple => P.Root.asInstanceOf[P[T]]
      case (x: String) *: xx => P.Static(make(xx).asInstanceOf[P[xx.type]], x).asInstanceOf[P[T]]
      case (x: Param[a]) *: xx => P.Variable(make(xx).asInstanceOf[P[xx.type]], x.name, x.converter).asInstanceOf[P[T]]
    }
  }
}

用法

object Main {
  import XX.*

  val link1 = make("foo", param[Int]("bar"), param[String]("bar"))
  val link2 = P.Root / "foo" / param[Int]("bar") / param[String]("bar"))
  //val link3 = makeBroken("foo", param[Int]("bar"), param[String]("bar")) //fails runtime with class cast exception
}

makemakeBroken导致警告

XX.scala:50:5: match may not be exhaustive.

It would fail on pattern case: _: *:[Any,Tuple]
 [50:5]

XX.scala:58:5: match may not be exhaustive.

It would fail on pattern case: *:(_, _)
 [58:5]
68de4m5k

68de4m5k1#

使用Scala编译器3.2.2版本编译代码时,我得到以下附加警告:

-- Unchecked Warning: /Users/mbovel/scala-snippets-2/so76236049.scala:50:11 -------
50 |      case (x *: xx): (String *: xs) => P.Static(makeBroken[xs](xx).asInstanceOf[P[xs]], x)
   |           ^
   |the type test for (String *: xs) @xs cannot be checked at runtime because its type arguments can't be determined from T

这解释了你得到的异常:编译器无法在运行时检查类型(String *: xs)。通常,由于类型擦除,Scala中的运行时类型检查无法区分参数化类型。因此,编译器生成的代码只检查t是否是元组,然后直接将t.head转换为String,而不检查是否是元组。您可以通过打印生成的代码来查看它,例如使用scala -Xprint:genBCode so76236049.scala

...
def makeBroken(t: Product): Object = 
      {
        (matchResult11[XX.XX$P]: 
          {
            case val x14: Product = t
            if x14.eq(EmptyTuple) then 
              {
                return[matchResult11] XX$P#Root
              }
             else ()
            if scala.runtime.Tuples.isInstanceOfNonEmptyTuple(x14) then 
              {
                case val x20: Tuple2 = *:.unapply(x14:Product)
                case val x: String = x20._1().asInstanceOf[String]
...

要避免这个缺点,并且仍然让makeBroken方法作为Elem[T]进行类型检查,最好的办法是将匹配类型和术语级匹配分为两个级别:

type Elem[X <: Tuple] = X match {
  case EmptyTuple => P.Root.type
  case x *: xs =>
      x match {
          case String => P.Static[xs]
          case Param[y] => P.Variable[y, xs]
      }
}

def makeUnbroken[T <: Tuple](t: T): Elem[T] = {
  t match {
    case _: EmptyTuple => P.Root
    case t: (x *: xs) =>
      t.head match {
        case head: String => P.Static(makeUnbroken[xs](t.tail).asInstanceOf[P[xs]], head)
        case head: Param[t] => P.Variable(make(t.tail).asInstanceOf[P[xs]], head.name, head.converter)
      }
  }
}

相关问题