Duncan Green
Duncan Green

Reputation: 109

How should I create subtypes of Int in Scala?

How can I create multiple types that are essentially Ints, i.e can get the int value, can use math operators like + BUT where instances of different types can not be mixed.

For example:

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

There could be hundreds of these types and I do not want to have to implement operators like + in each leaf class.

Ideally, I would like to avoid all implicit conversion mechanisms (personal preference only). Additional types should not require altering existing types.

Upvotes: 4

Views: 296

Answers (2)

user
user

Reputation: 7604

Here's a solution in Scala 3 that I don't think uses boxing/unboxing:

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

Testing:

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

The asInstanceOf is unchecked and shouldn't affect performance. This design should also keep the Ints from being boxed, but I can't guarantee that, and it also depends on how you use this. Another nice thing about this is that every new type requires only 2 more lines of code. And to make adding new methods easier, you can probably also make a new method of your own to shorten asInstanceOf[T].

Upvotes: 3

Alexey Romanov
Alexey Romanov

Reputation: 170815

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)
}

I was desperately trying to get around having to repeat override protected def apply(value: Int) = ... all the time

You can make it a constructor parameter then. Slightly less efficient but probably won't matter in practice:

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)

I originally had

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]

but it has at least 2 problems for this usecase:

  1. Density(100).toString is TaggedInt(100) instead of Density(100);
  2. Density(100) is equal to Variability(100).

Upvotes: 2

Related Questions