用于检查参数值的 Scala 类型约束

Scala type constraint to check argument values

我正在尝试在 Scala 中实现 Conway 的 surreal numbers。超现实数是递归定义的——作为一对超现实数集,称为左和右,使得右集中的任何元素都不小于或等于左集中的任何元素。这里超现实数之间的“小于或等于”关系也是递归定义的:我们说 xy if

我们从将零定义为一对空集开始,然后用零定义1和-1,依此类推。

我不知道如何在编译时强制定义超现实数。这就是我现在拥有的:

case class SurrealNumber(left: Set[SurrealNumber], right: Set[SurrealNumber]) {
  if ((for { a <- left; b <- right; if b <= a } yield (a, b)).nonEmpty)
    throw new Exception
  def <=(other: SurrealNumber): Boolean =
    !this.left.exists(other <= _) && !other.right.exists(_ <= this)
}

val zero = SurrealNumber(Set.empty, Set.empty)
val one = SurrealNumber(Set(zero), Set.empty)
val minusOne = SurrealNumber(Set.empty, Set(zero))

assert(zero <= zero)
assert((zero <= one) && !(one <= zero))
assert((minusOne <= zero) && !(zero <= minusOne))

当参数无效时,如 SurrealNumber(Set(one), Set(zero)),此代码将抛出运行时异常。是否可以将有效性检查表示为类型约束,以便 SurrealNumber(Set(one), Set(zero)) 无法编译?

您可以定义一个宏以便在编译时执行计算

case class SurrealNumber private(left: Set[SurrealNumber], right: Set[SurrealNumber]) {
  def <=(other: SurrealNumber): Boolean =
    !this.left.exists(other <= _) && !other.right.exists(_ <= this)
}

object SurrealNumber {
  def unsafeApply(left: Set[SurrealNumber], right: Set[SurrealNumber]): SurrealNumber =
    new SurrealNumber(left, right)

  def apply(left: Set[SurrealNumber], right: Set[SurrealNumber]): SurrealNumber =
    macro applyImpl

  def applyImpl(c: blackbox.Context)(left: c.Tree, right: c.Tree): c.Tree = {
    import c.universe._
    def eval[A](t: Tree): A = c.eval(c.Expr[A](c.untypecheck(t)))
    val l = eval[Set[SurrealNumber]](left)
    val r = eval[Set[SurrealNumber]](right)
    if ((for { a <- l; b <- r; if b <= a } yield (a, b)).nonEmpty)
      c.abort(c.enclosingPosition, "invalid surreal number")
    else q"SurrealNumber.unsafeApply($left, $right)"
  }
}

但问题是,虽然

SurrealNumber(Set.empty, Set.empty)

zero 的编译时值,但是

SurrealNumber(Set(zero), Set.empty)
SurrealNumber(Set.empty, Set(zero))

oneminusOne 的运行时值,编译器无法访问它们。所以

SurrealNumber(Set(SurrealNumber(Set.empty, Set.empty)), Set.empty)
SurrealNumber(Set.empty, Set(SurrealNumber(Set.empty, Set.empty)))

编译但

SurrealNumber(Set(zero), Set.empty)
SurrealNumber(Set.empty, Set(zero))

不要。


所以您应该重新设计 SurrealNumber 以提高类型级别。例如

import shapeless.{::, HList, HNil, IsDistinctConstraint, OrElse, Poly1, Poly2, Refute, poly}
import shapeless.ops.hlist.{CollectFirst, LeftReducer}
import shapeless.test.illTyped

class SurrealNumber[L <: HList : IsDistinctConstraint : IsSorted, 
                    R <: HList : IsDistinctConstraint : IsSorted](implicit
  notExist: Refute[CollectFirst[L, CollectPoly[R]]]
)

trait LEq[S, S1]
object LEq {
  implicit def mkLEq[S,  L  <: HList,  R <: HList, 
                     S1, L1 <: HList, R1 <: HList](implicit
    ev:        S  <:< SurrealNumber[L, R],
    ev1:       S1 <:< SurrealNumber[L1, R1],
    notExist:  Refute[CollectFirst[L, FlippedLEqPoly[S1]]],
    notExist1: Refute[CollectFirst[R1, LEqPoly[S]]]
  ): S LEq S1 = null
}

trait CollectPoly[R <: HList] extends Poly1
object CollectPoly {
  implicit def cse[R <: HList, LElem](implicit 
    exist: CollectFirst[R, LEqPoly[LElem]]
  ): poly.Case1.Aux[CollectPoly[R], LElem, Unit] = null
}

trait LEqPoly[FixedElem] extends Poly1
object LEqPoly {
  implicit def cse[FixedElem, Elem](implicit 
    leq: Elem LEq FixedElem
  ): poly.Case1.Aux[LEqPoly[FixedElem], Elem, Unit] = null
}

trait FlippedLEqPoly[FixedElem] extends Poly1
object FlippedLEqPoly {
  implicit def cse[FixedElem, Elem](implicit 
    leq: FixedElem LEq Elem
  ): poly.Case1.Aux[FlippedLEqPoly[FixedElem], Elem, Unit] = null
}

object isSortedPoly extends Poly2 {
  implicit def cse[Elem, Elem1](implicit 
    leq: Elem LEq Elem1
  ): Case.Aux[Elem, Elem1, Elem1] = null
}
type IsSorted[L <: HList] = (L <:< HNil) OrElse LeftReducer[L, isSortedPoly.type]

val zero = new SurrealNumber[HNil, HNil]
val one = new SurrealNumber[zero.type :: HNil, HNil]
val minusOne = new SurrealNumber[HNil, zero.type :: HNil]
illTyped("new SurrealNumber[one.type :: HNil, zero.type :: HNil]")
new SurrealNumber[zero.type :: HNil, one.type :: HNil]