如何对 Scala 方法的值强制执行编译时约束?
How can I enforce compile-time constraints on values for Scala methods?
我想在编译时强制约束 Scala 方法的参数值。
例如:
case class Foo(numberOfFoo: Int, ...)
numberOfFoo
是上面的 Int
,但我真的很想让它成为一个正整数。我已经尝试过 classes ,例如 PositiveInt 来强制执行此操作,但这只是将检查推到另一个 class 仍然没有编译时检查的地方。
使用上面的例子,我想要这个:
val n: Int = ...
val f: Foo = Foo(n)
如果 n > 0
则编译,如果 n <= 0
则不编译。我不希望实例化代码必须处理可能的异常,处理 Option[Foo]
,或者以 Foo
结束 Foo.numberOfFoo != n
(即我不想使用输入参数的绝对值)。
更新:感谢您提供有用的信息。正如我所担心的那样。大多数情况下,我希望能够指定必须具有正整数大小的东西的大小。所以这似乎是最好的方法:
case class Foo(bar: Bar) {val n = bar size}
您将不得不使用 refined 库。这是不求助于 Nat
或其他类型技巧的唯一方法。来自自述文件中的示例:
import eu.timepit.refined._
import eu.timepit.refined.api.Refined
import eu.timepit.refined.auto._
import eu.timepit.refined.numeric._
// This refines Int with the Positive predicate and checks via an
// implicit macro that the assigned value satisfies it:
scala> val i1: Int Refined Positive = 5
i1: Int Refined Positive = 5
// If the value does not satisfy the predicate, we get a meaningful
// compile error:
scala> val i2: Int Refined Positive = -5
<console>:22: error: Predicate failed: (-5 > 0).
val i2: Int Refined Positive = -5
另一种方法是使用 shapeless
库,然后使用 Nat
。限制是您需要在编译时基本上使用已知常量实例化那些 Foo
实体。
import shapeless.ops.nat_
import shapeless.nat._
case class Foo[T <: Nat](numberOfFoo: Int)(implicit ev: GT[T, _0]
object Foo {
// The problem is this won't work.
def apply[T <: Nat](n: Int): Foo[T] = Foo(Nat(n))
}
只有像这样使用才会起作用:
Foo(_1)
其中 _1
来自 shapeless.nat._
。如果您深入研究实施,即使您没有打算,0
部分也会被强制执行 by shapeless:
if (n < 0) c.abort(c.enclosingPosition, s"A Nat cannot represent $n")
坚持简单的事情
然而,这非常麻烦,因为无论您采用哪种方法,它都将依赖于宏,而宏不能真正起作用,除非该值在编译时已知。如果最简单的委托方法不再有效,这可能会变得非常有限。
实际上,绕过这种方法并使用常规方法可能更有效。无论您使用 shapeless 还是上面提到的精炼库,故事都不会改变,因此对于正常用例,进行运行时验证可能更干净。
我想在编译时强制约束 Scala 方法的参数值。
例如:
case class Foo(numberOfFoo: Int, ...)
numberOfFoo
是上面的 Int
,但我真的很想让它成为一个正整数。我已经尝试过 classes ,例如 PositiveInt 来强制执行此操作,但这只是将检查推到另一个 class 仍然没有编译时检查的地方。
使用上面的例子,我想要这个:
val n: Int = ...
val f: Foo = Foo(n)
如果 n > 0
则编译,如果 n <= 0
则不编译。我不希望实例化代码必须处理可能的异常,处理 Option[Foo]
,或者以 Foo
结束 Foo.numberOfFoo != n
(即我不想使用输入参数的绝对值)。
更新:感谢您提供有用的信息。正如我所担心的那样。大多数情况下,我希望能够指定必须具有正整数大小的东西的大小。所以这似乎是最好的方法:
case class Foo(bar: Bar) {val n = bar size}
您将不得不使用 refined 库。这是不求助于 Nat
或其他类型技巧的唯一方法。来自自述文件中的示例:
import eu.timepit.refined._
import eu.timepit.refined.api.Refined
import eu.timepit.refined.auto._
import eu.timepit.refined.numeric._
// This refines Int with the Positive predicate and checks via an
// implicit macro that the assigned value satisfies it:
scala> val i1: Int Refined Positive = 5
i1: Int Refined Positive = 5
// If the value does not satisfy the predicate, we get a meaningful
// compile error:
scala> val i2: Int Refined Positive = -5
<console>:22: error: Predicate failed: (-5 > 0).
val i2: Int Refined Positive = -5
另一种方法是使用 shapeless
库,然后使用 Nat
。限制是您需要在编译时基本上使用已知常量实例化那些 Foo
实体。
import shapeless.ops.nat_
import shapeless.nat._
case class Foo[T <: Nat](numberOfFoo: Int)(implicit ev: GT[T, _0]
object Foo {
// The problem is this won't work.
def apply[T <: Nat](n: Int): Foo[T] = Foo(Nat(n))
}
只有像这样使用才会起作用:
Foo(_1)
其中 _1
来自 shapeless.nat._
。如果您深入研究实施,即使您没有打算,0
部分也会被强制执行 by shapeless:
if (n < 0) c.abort(c.enclosingPosition, s"A Nat cannot represent $n")
坚持简单的事情
然而,这非常麻烦,因为无论您采用哪种方法,它都将依赖于宏,而宏不能真正起作用,除非该值在编译时已知。如果最简单的委托方法不再有效,这可能会变得非常有限。
实际上,绕过这种方法并使用常规方法可能更有效。无论您使用 shapeless 还是上面提到的精炼库,故事都不会改变,因此对于正常用例,进行运行时验证可能更干净。