Reputation: 109
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
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
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 Int
s 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
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:
Density(100).toString
is TaggedInt(100)
instead of Density(100)
;Density(100)
is equal to Variability(100)
.Upvotes: 2