Suppose that, whenever type A can represent a less-than-perfectly-precise measurement of a physical value, I'll have an instance of Imprecise[A].
trait Imprecise[A]:
type Precision
val ord: Ordering[Precision]
def precision(a: A): Precision
For example, this Bounds type has a given instance in its companion object:
case class Bounds[N: Numeric](min: N, max: N):
if summon[Ordering[N]].compare(min, max) > 1 then
throw new IllegalArgumentException
object Bounds:
import scala.math.Numeric.Implicits.infixNumericOps
import scala.math.Ordering.Implicits.infixOrderingOps
given [N: Numeric]: Imprecise[Bounds[N]] with
type Precision = N
val ord = summon[Ordering[N]]
def precision(rng: Bounds[N]) = rng.max - rng.min
Now my program can start dealing with the physical features that need to be observed:
trait Feature[A: Imprecise]:
type Observation = A
val imprecise = summon[Imprecise[A]]
type Precision = imprecise.Precision
def within(bound: Precision) = new RequiredFeature(this, bound)
class RequiredFeature(val feature: Feature[_], val min_precision: feature.Precision)
case class Temperature(chamber_id: Int) extends Feature[Bounds[Double]]
case class Pressure(chamber_id: Int) extends Feature[Bounds[Float]]
But when, at last, I try to make a required feature:
val rf = Temperature(3).within(7.0)
the compiler complains
Found: (7.0d : Double)
Required: ?1.Precision
where: ?1 is an unknown value of type exp.Temperature
val rf = Temperature(3).within(7.0)
The code written so far can be made to compile if I forego any path-dependent types and pass the precision around everywhere as a regular generic type parameter. But what if I don't want to do that? Is there some strategy like the Aux pattern that will help the compiler see that this is a valid call to within?