Search code examples
scaladomain-driven-designscala-3

Define new type whose domain is subset of another type in Scala


Using the example of even numbers, I would like to define the Even type whose domain is even integers using the function val isEven(i: Int): Boolean = i % 2 == 0

How would I do this in Scala 3 using the opaque keyword...

opaque type Even = ??? // use Int and filter using isEven or if that does not work,

type Even = ???

The rationale is to use functions as a building block to create new types, rather that complicating matters by using newtype as scala-even-type-number or using Refined Types


Solution

  • Something like the following:

    object Evens:
      opaque type Even = Int
    
      object Even:
        def apply(i: Int): Even = if i % 2 == 0 then i else sys.error(s"$i is odd")
        def unsafe(i: Int): Even = i
        def safe(i: Int): Option[Even] = Option.when(i % 2 == 0)(i)
    
      extension (x: Even)
        def toInt: Int = x
        def +(y: Even): Even = x + y
        def *(y: Int):  Even = x * y
        def half: Int = x / 2
    end Evens
    
    import Evens.*
    // val x: Even = 2 // doesn't compile
    val x: Even = Even(2)
    // val x: Even = Even(3) // RuntimeException: 3 is odd
    

    https://docs.scala-lang.org/scala3/book/types-opaque-types.html

    https://docs.scala-lang.org/scala3/reference/other-new-features/opaques.html

    If you'd like Even(3) to fail at compile time rather than runtime then replace def apply(i: Int): Even = ... with one of the following implementations

    import scala.compiletime.{error, erasedValue, constValue, codeOf, summonFrom}
    import scala.compiletime.ops.int.%
    import scala.compiletime.ops.any.{==, ToString}
    
    inline def apply[I <: Int with Singleton](inline i: I): Even =
      inline erasedValue[I % 2] match
        case _: 0 => i
        case _    => error(codeOf(i) + " is odd")
    
    inline def apply[I <: Int with Singleton](inline i: I): Even =
      inline if constValue[I % 2] == 0 then i else error(codeOf(i) + " is odd")
    
    inline def apply[I <: Int with Singleton](inline i: I): Even = 
      inline erasedValue[I % 2 == 0] match
        case _: true => i
        case _       => error(constValue[ToString[I]] + " is odd")
    
    inline def apply[I <: Int with Singleton](inline i: I): Even = 
      summonFrom {
        case _: (I % 2 =:= 0) => i
        case _                => error(constValue[ToString[I]] + " is odd")
      }
    
    inline def apply(inline i: Int): Even =
      inline if i % 2 == 0 then i else error(codeOf(i) + " is odd")
    

    https://docs.scala-lang.org/scala3/reference/metaprogramming/inline.html

    https://docs.scala-lang.org/scala3/reference/metaprogramming/compiletime-ops.html