我应该如何在 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 未选中,不应影响性能。这种设计应该还可以防止Ints 被装箱,但我不能保证,这也取决于你如何使用它。另一个好处是每个新类型只需要多两行代码。为了使添加新方法更容易,您还可以创建自己的新方法来缩短 asInstanceOf[T]

【讨论】:

这看起来很有趣,但不希望使用反射来提高性能和 Liskov 合规性。 Odersky 说您可以通过模式匹配“恢复类型信息”,但目前尚不清楚如何针对新类型关闭这些匹配语句。 我认为 opaque 类型看起来像是解决此问题的可扩展方式。有没有办法避免instanceof? @DuncanGreen .asInstanceOf[T] 在这种情况下没有性能损失。它也非常安全,因为它被封装在一个模块中。所以没有外部用户需要自己打电话给instanceOf @DuncanGreen 这里没有反射,加上Matchable 特征,你不能在不透明类型上进行模式匹配并发现它们都是Ints。所以我认为这是一种相当安全的方法。 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).toStringTaggedInt(100) 而不是 Density(100); Density(100) 等于 Variability(100)

【讨论】:

我想出了同样的事情,但我拼命想避免不得不一直重复override protected def apply(value: Int) = ... 这几乎是我目前所拥有的。感谢您指出限制。最初它看起来像是通过从 AnyVal 继承来包装 int 的简单问题,但这会阻止进一步的继承,老实说,这似乎相当麻烦。

以上是关于我应该如何在 Scala 中创建 Int 的子类型?的主要内容,如果未能解决你的问题,请参考以下文章

如何在 Scala 中创建异构数组?

如何在scala中创建镶木地板?

如果我想将视图分解为更小的子视图,我应该在 ASP.NET MVC 中创建啥项目?

如何在 Databricks 的 PySpark 中使用在 Scala 中创建的 DataFrame

在 Scala 中创建最小堆的最简单和最有效的方法是啥?

Scala 如何将 Int 转换为 Double?