0

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 DataTypes, 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.

5
  • It might be helpful if you were to include some client code, i.e. your ideal of how this would be used. Commented Aug 28, 2020 at 6:28
  • @jwvh - thanks - I've updated the original post with an extra paragraph which hopefully adds a bit of clarity. If it's still not clear then do let me know. Commented Aug 28, 2020 at 7:19
  • 3
    So you've got a number of things going wrong here. You start with a recursive type definition, type ElemT <: Numeric[ElemT], which isn't bad in and of itself, but it can't be overridden with a simple type: type ElemT = Double. Then it looks like you want to allow something like DataPoint[WeightsData](n) but only where n is the numeric type pre-selected for WeightsData. It would be clearer if you included example code as requested. Don't describe, demonstrate. Add examples instead of explanations. Commented Aug 28, 2020 at 7:50
  • @jvwh This is a fair comment - I've expanded the example a bit (the original wasn't very good) and included some example user code. I guess what I am trying to achieve here is that I would like to prevent the user adding a DataPoint[WeightsData] to a DataPoint[PositionsData], or at least have different behaviour if this occurs. The type alone obviously doesn't prevent this since they are both double. Commented Aug 28, 2020 at 9:03
  • @jwvh - just to make sure I'm understanding you - both d1 and d2 are instances of DataPoint (not DataType), so should hold a value in the .point field. Commented Aug 28, 2020 at 9:45

2 Answers 2

3

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Yes - that seems perfect. Thank you very much indeed. And yes, this should be a compile-time error rather than a run-time error.
1

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) 

1 Comment

thank you - your comment about Numeric at the top actually clears up some of the confusion I've been having with it.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.