Home > Blockchain >  correct setup for opaque type with underlying Numeric/Ordering instances
correct setup for opaque type with underlying Numeric/Ordering instances

Time:01-16

unclear to me if this is in fact the same question as here or here, apologies if this is a duplicate.

i would like to define a type Ordinate which is simply an Int under-the-hood:

package world 

opaque type Ordinate = Int
given Ordering[Ordinate] with {
  def compare(x: Ordinate, y: Ordinate): Int = x.compare(y)
}

i would like to be able to leverage the Numeric[Int] and Ordering[Int] methods so that it would be easy to define methods such as

package world

import Ordinate.given

class Boundary(dims: List[(Ordinate, Ordinate)]) {
  def contains(o: Ordinate, dimension: Int): Boolean = {
    val (min, max) = dims(dimension)
    min <= o && o <= max
  }
}

...forgetting for the meantime that this would blow up if dims was empty, dimension < 0 or dims.length <= dimension.

when i try and set this up, i get compiler errors at the call site:

value <= is not a member of world.Ordinate, but could be made available as an extension method.

One of the following imports might fix the problem:

  import world.given_Ordering_Ordinate.mkOrderingOps
  import math.Ordering.Implicits.infixOrderingOps
  import math.Ordered.orderingToOrdered

more generally, it would be wicked cool if this were the case without any special given imports for files in the same package as Ordinate and even better, across the codebase. but that may be an anti-pattern that i've carried forward from my Scala 2 coding.

explicit given imports may be a better pattern but i'm still learning Scala 3 from Scala 2 here. i know if i created an implicit val o = Ordering.by(...) in the companion object of Ordinate in Scala 2, with Ordinate as a value class, i would get the effect i'm looking for (zero-cost type abstraction numeric behaviors).

anyhow, i'm guessing i'm just missing a small detail here, thank you for reading and for any help.

CodePudding user response:

Scala 3 has revised the rules for infix operators so that the author must (explicitly) expose infix operations such as x: T <= y: T for some custom type T.

I've found two ways to address this for an opaque type, both with drawbacks:

  1. at the call site, have import math.Ordering.Implicits.infixOrderingOps in scope, which brings in a given instance that converts Ordering[T] into infix comparators. drawback: any file that wants these comparators needs the import line, adding more import boilerplate as the number of files using this opaque type increases.
package world

import Ordinate.given
import math.Ordering.Implicits.infixOrderingOps  // <-- add this line

class Boundary(dims: List[(Ordinate, Ordinate)]) {
  def contains(o: Ordinate, dimension: Int): Boolean = {
    val (min, max) = dims(dimension)
    min <= o && o <= max
  }
}
  1. add an infix extension method for each comparator you want to expose. drawback here is boilerplate of having to write out the very thing we're trying not to duplicate in each file.
type Ordinate = Int
object Ordinate {
  extension (o: Ordinate) {
    infix def <=(x: Ordinate): Boolean = o <= x  // <-- add 'infix' here
  }
}

i'm guessing for those more experienced with large programs, these drawbacks are better than the drawbacks associated with anything more than this least permission approach to givens. but this still doesn't seem to deliver on the promise of opaque types as a zero-cost abstraction for numeric types. what seems to be missing is something like "import a given and treat it's methods as infix for my type".

  • Related