I like this approach (metric spaces are the appropriate construct to use here, of course) but am not sure about why all the type parameters are needed. I'm not familiar with Spire, I'm afraid, but presumably a MetricSpace[A] is just defined as follows:
trait MetricSpace[A] {
def distance(a1: A)((a2: A): Double
}
Together with the laws:
forall X, distance(X)(X) == 0
forall X, Y, distance(X)(Y) == diance(Y)(X)
forall X, Y, Z, distance(X)(Y) + distance(Y)(Z) <= distance(X)(Z)
We can then define =~= (which is now a measure of "closeness", rather than equality, so I'd drop the operator implying approximate equality)
trait Epsilon
def epsilon(d: Double): Double @@ Epsilon = d.asInstanceOf[Double @@ Epsilon]
implicit class MetricSpacePoint[A: MetricSpace](a: A) { def isCloseTo(y: A)(implicit val eps: Double @@ Epsilon) = MetricSpace[A].distance(a)(y).abs <= eps.abs } }
So that:
implicit val e = epsilon(0.001)
1 isCloseTo 1.1
I suppose you might want to tag the epsilon by your space (so that an epsilon intended for Euclidean space couldn't be mistaken for one in hyperbolic space). Perhaps you might use dependent types for this? That is:
trait MetricSpace[A] {
type Distance <: Double
def distance(a1: A)((a2: A): Distance
}
And:
implicit class MetricSpacePoint[A](a: A)(implicit M: : MetricSpace[A]) {
def isCloseTo(y: A)(implicit val eps: M.Distance) = M.distance(a)(y).abs <= eps.abs }
}
(That's probably not correct - I'm no replacement for Miles)
Chris
(This doesn't work in scalaz, where their definition of a metric space has an integral distance).