我应该如何在 Scala 中创建 Int 的子类型?
Posted
技术标签:
【中文标题】我应该如何在 Scala 中创建 Int 的子类型?【英文标题】:How should I create subtypes of Int in Scala? 【发布时间】:2021-03-28 05:44:20 【问题描述】:如何创建多个本质上是 Ints 的类型,即可以获取 int 值,可以使用 + BUT 等数学运算符,但不能混合不同类型的实例。
例如:
val density1 = new Density(100)
val density2 = new Density(200)
density1 + density2 should be(new Density(300))
val variability = new Variability(1)
variability.value should be(1)
density1 + variability // does not compile
可能有数百种这样的类型,我不想在每个叶类中实现像 + 这样的运算符。
理想情况下,我想避免所有隐式转换机制(仅限个人喜好)。其他类型不应要求更改现有类型。
【问题讨论】:
为什么不直接在 Int 上创建一个包装器? Scala 3 正在为这个用例获得opaque types。该页面描述了 Scala 2 的替代方案。 @AminMal 据我了解,问题是 如何 在Int
上创建一个包装器,因为“可能有数百种这样的类型,我不想实现每个叶类中的 + 等运算符"
我确实尝试过创建一个包装器。问题变成了如何混合数学运算符。可能有一种方法可以按特征提取运算符,例如可添加,但构建具有未知类型特征的新实例变得有问题。
@KarlBielefeldt opaque types 看起来像是可扩展的解决方案,我可以迁移到 Scala 3。
【参考方案1】:
这是 Scala 3 中我认为不使用装箱/拆箱的解决方案:
object Wrappers:
opaque type Wrapper = Int
extension[T <: Wrapper](t: T)(using util.NotGiven[T =:= Wrapper]):
def +(other: T): T = (t + other).asInstanceOf[T]
//other methods
opaque type Density <: Wrapper = Int
def Density(i: Int): Density = i
opaque type Variability <: Wrapper = Int
def Variability(i: Int): Variability = i
Try it in Scastie
测试:
val density1 = Density(1)
val density2 = Density(2)
val density3: Density = density1 + density2 //compiles
val check1: Variability = density1 + density2 //doesn't compile
val variability = Variability(1)
val check2 = (variability: Wrapper) + density2 //doesn't compile
val check3 = variability + density2 //doesn't compile
println(density1) //1
println(density2) //2
println(density3) //3
asInstanceOf
未选中,不应影响性能。这种设计应该还可以防止Int
s 被装箱,但我不能保证,这也取决于你如何使用它。另一个好处是每个新类型只需要多两行代码。为了使添加新方法更容易,您还可以创建自己的新方法来缩短 asInstanceOf[T]
。
【讨论】:
这看起来很有趣,但不希望使用反射来提高性能和 Liskov 合规性。 Odersky 说您可以通过模式匹配“恢复类型信息”,但目前尚不清楚如何针对新类型关闭这些匹配语句。 我认为 opaque 类型看起来像是解决此问题的可扩展方式。有没有办法避免instanceof? @DuncanGreen.asInstanceOf[T]
在这种情况下没有性能损失。它也非常安全,因为它被封装在一个模块中。所以没有外部用户需要自己打电话给instanceOf
。
@DuncanGreen 这里没有反射,加上Matchable
特征,你不能在不透明类型上进行模式匹配并发现它们都是Int
s。所以我认为这是一种相当安全的方法。
wiki.c2.com/?LiskovSubstitutionPrinciple 我认为您的解决方案可能比较安全,因为我看不出 (t + other) 可以如何解决除 T 以外的任何问题。通常从基本类型“向下转换”到更具体的类型违反了 Liskov,但我认为在这种情况下你不会这样做。一切都很好!【参考方案2】:
trait TaggedInt[T <: TaggedInt[T]]
val value: Int
protected def apply(value: Int): T
def +(other: T) = apply(value + other.value)
// etc.
case class Density(value: Int) extends TaggedInt[Density]
override protected def apply(value: Int) = Density(value)
我拼命想避免一直重复
override protected def apply(value: Int) = ...
然后您可以将其设为构造函数参数。效率稍低,但在实践中可能无关紧要:
abstract class TaggedInt[T <: TaggedInt[T]](constructor: Int => T)
val value: Int
def +(other: T) = constructor(value + other.value)
// etc.
case class Density(value: Int) extends TaggedInt[Density](Density)
我原来有
case class TaggedInt[Tag](value: Int) extends AnyVal
def +(other: TaggedInt[Tag]) = TaggedInt[Tag](value + other.value)
// etc.
trait DensityTag
type Density = TaggedInt[DensityTag]
trait VariabilityTag
type Variability = TaggedInt[VariabilityTag]
但这个用例至少有两个问题:
Density(100).toString
是 TaggedInt(100)
而不是 Density(100)
;
Density(100)
等于 Variability(100)
。
【讨论】:
我想出了同样的事情,但我拼命想避免不得不一直重复override protected def apply(value: Int) = ...
这几乎是我目前所拥有的。感谢您指出限制。最初它看起来像是通过从 AnyVal 继承来包装 int 的简单问题,但这会阻止进一步的继承,老实说,这似乎相当麻烦。以上是关于我应该如何在 Scala 中创建 Int 的子类型?的主要内容,如果未能解决你的问题,请参考以下文章
如果我想将视图分解为更小的子视图,我应该在 ASP.NET MVC 中创建啥项目?