Search code examples
scalatypeclassscala-3opaque-types

correct setup for opaque type with underlying Numeric/Ordering instances


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.


Solution

  • It is legal to export implicit methods, see https://docs.scala-lang.org/scala3/reference/other-new-features/export.html .

    Thus, instead of importing infixOrderingOps at call site, it is possible to export it at definition site. In our case in the companion object of Ordinate.

    Definition site:

    object world:
        opaque type Ordinate = Int
        given Ordering[Ordinate] with
            def compare(x: Ordinate, y: Ordinate): Int = x.compare(y)
        object Ordinate :
            // Creates implicit infix ordering methods 
            export math.Ordering.Implicits.infixOrderingOps
    

    Call Site

    object usage :
        import world.Ordinate
        // vvvvvv ** Not Needed : Ordering[Ordinate] 
        // import world.given
        // vvvvvv ** Not Needed : infixOrderingOps in scope via the export
        // import math.Ordering.Implicits.infixOrderingOps
        def lessThan(x:Ordinate,y:Ordinate):Boolean = x<=y // Works
    

    It is also legal to export extension methods.

    object OrderingExtensions:
        extension[T](using ord:Ordering[T]) (x:T)
            def <= ( y:T ) : Boolean =  ord.lteq(x,y)
            // ... and all the others
    
    object world:
        opaque type Ordinate = Int
        given Ordering[Ordinate] with
                def compare(x: Ordinate, y: Ordinate): Int = x.compare(y)
        object Ordinate :
            // vvv Exporting extensions methods
            export OrderingExtensions.*
            
    
    object usage :
        // Required :          vvvvv extension uses Ordering[Ordinate] 
        import world.{Ordinate,given}
        def lessThan(x:Ordinate,y:Ordinate):Boolean = x<=y // Works