I am looking for a way to define a retroactive conformance to a trait. In other words, I’d like to make an existing type conform to a new trait. For example, imagine that I declare this:
trait BitSet {
def isSet(position: Int): Bool
}
Is there a way I can make Long conform to BitSet?
My goal is to be able to define generic algorithms in terms of a new trait (e.g., BitSet) and use them on already existing types (e.g. Long). I suppose I could wrap instances in new classes conforming to my trait, but then I would be compelled to wrap/unwrap these representations all the time.
My apologies if the terms I’m using are a little foreign. Don’t hesitate to request clarifications as necessary.
This is what typeclasses are for, and one of the reasons many folks prefer them to traditional OO style in many situations. Unlike inheritance-based behavior, a typeclass can be adapted to any appropriate class, whether new or legacy.
So basically, instead of defining an OO trait BitSet, you would create a typeclass BitSet[T], whose instance for a given type T provides the implementation of isSet() for T. You can then couple that with a syntax adapter (an implicit class in Scala 2; I forget the right syntax for Scala 3), and you wind up with something that you call much like the OO version, but is more flexible.
Someone else will have to chime in here – I haven’t done enough Scala 3 to know the right approach. In Scala 2 it would just be another member in the instance:
implicit object LongBitSet = new BitSet[Long] {
type Slice = LongSlice
def slice(x: Long, range: Range): Slice = LongSlice()
}
But I’m not sure offhand how to best express that type implementation in Scala 3. (Implicits were heavily rewritten between 2 and 3, to reduce boilerplate and make the system more usable.)
You can simply implement the type member in the given definition:
given BitSet[Long] with
type Slice = LongSlice
extension (x: Long) def slice(range: Range) = LongSlice()
But be aware that Slice will only be known to be LongSlice in places in the code where you statically know that you have this specific BitSet[Long] instance. E.g. take this pretty silly example:
def foo(using b: BitSet[Long]): b.Slice = ???
In the method body of foo you can’t know whether or not the BitSet[Long] instance you are “using” is the instance that you defined with type Slice = LongSlice. However if you call val slice = foo then slice should have type LongSlice because the compiler knows which instance it passed into foo.