Reputation: 1851
I've been grappling with this issue for the past 24 hours with little success, and already posted a couple of related questions, so apologies if anyone has seen it before. I think what I want to do is conceptually quite simple, and looks like this:
sealed trait DataType {
type ElemT <: Numeric[ElemT]
}
trait PositionsData extends DataType { type ElemT = Double }
trait WeightsData extends DataType { type ElemT = Double }
trait Error
case class TypeDoesntMatch() extends Error
case class DataPoint[T <: DataType] (
point: T#ElemT
) {
def addToDataPoint(addTo: DataPoint[T]): Either[Error, DataPoint[T]] =
Right(DataPoint[T](this.point + addTo.point))
// above method generates the error type mismatch; found: T#ElemT required: String
def addToDataPoint(addTo: DataPoint[_]): Either[Error, DataPoint[T]] = Left(TypeDoesntMatch())
}
// Example user behaviour -
val d1 = DataPoint[PositionsData](1.1)
val d2 = DataPoint[PositionsData](2.2)
val d3 = DataPoint[WeightsData](3.3)
d1.addToDataPoint(d2) // should return Right(DataPoint[PositionsData](3.3))
d3.addToDataPoint(d2) // should return Left(TypeDoesntMatch())
The idea is to let the user of the library choose from a pre-defined list of (numeric) data types (the DataType
trait) when creating (say) a DataPoint
. The author/maintainer of the library can then set exactly what numeric data type each DataType
uses, away from the user's view.
So I'd like to define an enumeration (in the general sense) of DataType
s, each with their own associated numeric type. I'd then like to be able to pass these to the DataPoint case class as a generic. The bits I'm having trouble with are
a) I can't figure out how to constrain ElemT
to Numeric
types only - the <: Numeric[ElemT]
in the code below doesn't work, I think because, as others have pointed out, Double
is not a subclass of Numeric[Double]
.
b) I am having a bit of trouble figuring out how to introduce the necessary implicit conversion so that Numeric.plus
is used in this.point + addTo.point
.
I'm not at all fussy about how I achieve this and if my approach looks totally wrong then I'd be happy to be told that. Sincere thanks to anyone who can help me escape this on-going type-mare.
Upvotes: 0
Views: 77
Reputation: 51271
If I understand the issues here, one solution might be to move the Numeric
requirement to the same place as the DataType
requirement (which is where the addition takes place anyway).
sealed trait DataType { type ElemT }
trait PositionsData extends DataType { type ElemT = Double }
trait WeightsData extends DataType { type ElemT = Double }
case class DataPoint[T <: DataType](point: T#ElemT
)(implicit ev:Numeric[T#ElemT]) {
import ev._
def addToPoint(addTo: T#ElemT): DataPoint[T] =
DataPoint[T](this.point + addTo)
}
This allows some type restrictions...
// DataPoint[WeightsData]("2.2") <- won't compile
val dp = DataPoint[WeightsData](2.2)
dp.addToPoint(4.1)
//res0: DataPoint[WeightsData] = DataPoint(6.3)
...but it won't disallow numeric widening. (There's been some talk of removing this from the language, but, until then ...)
val dp = DataPoint[WeightsData](2) //Int
dp.addToPoint(48) //Int
//res0: DataPoint[WeightsData] = DataPoint(50.0) <- Double
UPDATE
With the updated user examples (kinda wish you had started with that info) my suggestion remains: Move the Numeric
type restriction to the DataPoint
class.
. . . //as before
case class DataPoint[T <: DataType](point: T#ElemT
)(implicit ev:Numeric[T#ElemT]) {
import ev._
def addToDataPoint(addTo: DataPoint[T]): DataPoint[T] =
DataPoint[T](this.point + addTo.point)
}
// Example user behaviour -
val d1 = DataPoint[PositionsData](1.1)
val d2 = DataPoint[PositionsData](2.2)
val d3 = DataPoint[WeightsData](3.3)
d1.addToDataPoint(d2)
//d3.addToDataPoint(d2) <- won't compile
I don't see any advantage in logging a runtime error, via Either[_,_]
, instead of a compile-time error. In fact, I think it would be both more difficult to achieve and less useful.
Upvotes: 3
Reputation: 850
The problem of Numeric
, as I understand, that it is just an implicit object and it looks like a typeclass and Numeric[Double]
is a typeclass instance.
So you can place ElemT to type parameter and constrain it with context bound to Numeric
:
case class DataPoint[ElemT: Numeric] (point: ElemT) {
def addToPoint(addTo: ElemT): DataPoint[ElemT] = {
val sum = implicitly[Numeric[ElemT]].plus(this.point, addTo)
DataPoint[ElemT](sum)
}
}
DataPoint(9.1).addToPoint(2.3)
Or if you for some reason still need sealed trait DataType
with type member.
You can
sealed trait DataType {
type ElemT
}
trait PositionsData extends DataType { type ElemT = Double }
trait WeightsData extends DataType { type ElemT = Double }
case class DataPoint[T <: DataType] (point: T#ElemT)(implicit numericInstance: Numeric[T#ElemT]) {
def addToPoint(addTo: T#ElemT): DataPoint[T] = {
val sum = numericInstance.plus(this.point, addTo)
DataPoint[T](sum)
}
}
DataPoint[PositionsData](9.1).addToPoint(2.3)
Upvotes: 1