在Scala中,是否可以通过创建类似PositiveInt的东西来限制Int,并进行编译时检查?

7vhp5slm  于 2022-11-23  发布在  Scala
关注(0)|答案(6)|浏览(195)

是否可以创建一个 restricted Int(例如PositiveInt)并对其进行编译时检查?换句话说,是否可以定义一个方法,例如:

def myMethod(x: PositiveInt) = x + 1

然后有这样的东西:

myMethod(-5) // does not compile
myMethod(0)  // does not compile
myMethod(5)  // compiles

如果这是可能的,我应该如何开始定义PositiveInt,我的意思是,在Scala中是否有一个方便的技术?

ltskdhd1

ltskdhd11#

这种东西叫做dependent typing,Scala中没有。

brc7rcf0

brc7rcf02#

如果你有编译时依赖的类型检查,你解决了停机问题,或者你的编译器并不总是保证完成编译,或者你的编程语言的语法需要图灵完成,所有这些都是荒谬的事情。
下面是在 runtime 期间依赖类型化的合理替代方法

object ScalaDependentTyping extends App {

  implicit class NaturalNumber(val x: Int) {
    assume(x >= 0)
  }

  implicit def toInt(y: NaturalNumber): Int = y.x

  def addOne(n: NaturalNumber) = n+1

  println(addOne(0))
  println(addOne(1))
  println(addOne(2))

  println(addOne(-1))  //exception
}
mlmc2os5

mlmc2os53#

可以按以下方式在原始类型上使用标记特征

trait Positive
type PosInt = Int with Positive
def makePositive(i: Int): Option[PosInt] = if(i < 0) None else Some(i.asInstanceOf[PosInt])
def succ(i: PosInt): PosInt = (i + 1).asInstanceOf[PosInt]

但是,在编写makePositive(-5)时只会出现运行时错误,而在编写succ(5)时会出现编译时错误。
也许可以编写一个编译器插件,将正整数文字“提升”到一个标记类型。

编辑

我还没有测试过以这种方式标记基元类型是否有任何运行时开销。

jdzmm42g

jdzmm42g4#

其他的答案主要是关于创建一个“正数”类型,该类型以编译器可以保证正确性的方式与一个“数字”类型相关联。例如,使用依赖类型,你可以证明像abs这样的函数的实现是正确的,而使用scala则不能。
不过,您可以建置只表示自然数的型别。例如:

abstract class Pos {
  def toInt: Int
}

case object Zero extends Pos {
  def toInt: Int = 0
}

case class Next(x: Pos) extends Pos {
  def toInt: Int = 1 + x.toInt
}

object Pos {
  def apply(x: Int): Pos =
    x match {
      case n if (n < 0) => throw new IllegalArgumentException(s"$x is not a positive integer")
      case 0 => Zero
      case n => Next(Pos(n-1))
    }
}

这与ziggystar提出的方法类似,不同之处在于类型的正确性是由它自己的构造来保证的,而不是由构造函数来保证的,也就是说,用这种类型不可能表示负数。
这种方法可能不适合您的目的。您要么需要实现它的所有操作,要么需要附带Int值,这样就相当于只进行一次运行时检查,因为您失去了通过这种方式表示正整数所赢得的所有类型安全。
由于Pos.apply不是类型安全的,所以在

myMethod(-5)
ac1kyiln

ac1kyiln5#

是的。它们被称为精炼类型。请检查:https://github.com/fthomas/refined以获取更多详细信息。
不需要自己实现任何东西,没有apply方法等。只需使用它..:)

g6baxovj

g6baxovj6#

有几种方法可以避免Int为负,但它总是有一些缺点。精炼库非常强大,但据我所知,目前它与Scala 3仍然存在一些兼容性问题。
不过,我对Scala 3非常满意,因为现在可以使用inline关键字来防止对负数进行编译,如下所示:

opaque type PositiveInt <: Int = Int

object PositiveInt:
  inline
  def apply(inline n: Int): PositiveInt =
    inline if n == 0
    then compiletime.error("Impossible to build PositiveInt(0)")
    else if n < 0
    then compiletime.error("Impossible to build PositiveInt(-n): negative value.")
    else n: PositiveInt

  def make(n: Int): Option[PositiveInt] =
    Option.when(n > 0)(n)

  extension (nOpt: Option[PositiveInt])
    inline
    def orDefault[T <: PositiveInt](default: T): PositiveInt =
      nOpt.getOrElse(default)

  extension (inline n: Int)
    inline
    def asPositive: PositiveInt =
      PositiveInt(n)

好的IDE(如IntelliJ)和编译器都会让你知道,如果你试图做一些被禁止的事情,那就有错误。

import PositiveInt.*

// Compiles
val a = PositiveInt(4)
val b: Int = a
val c = 99.asPositive
val d: Int = -5
val e = PositiveInt.make(d).orDefault(c) //Assigns: 99

// Won't compile
val e = PositiveInt(0) //Compile-time error: Impossible to build PositiveInt(0)
val f = -2.asPositive //Compile-time error: Impossible to build PositiveInt(-n): negative value.

相关问题