The only way I can think of doing this, without creating a wrapper class, is to use scala 3's type unions like this
type Even = 0 | 2 | 4 | 6 | 8
val even : Even = 4
but that obviously has a limit. Is there a way to create the "entire" range?
As a follow up, what about for other ranges? Is there some way to create a function that restricts the type in some arbitrary way (as dangerous as that sounds)?
CodePudding user response:
You can create a newtype with a smart constructor. Several ways to do it.
First, manually, to show how it work:
trait Newtype[T] {
type Type
protected def wrap(t: T): Type = t.asInstanceOf[Type]
protected def unwrap(t: Type): T = t.asInstanceOf[T]
}
type Even = Even.Type
object Even extends Newtype[Int] {
def parse(i: Int): Either[String, Even] =
if (i % 2 == 0) Right(wrap(i))
else Left(s"$i is odd")
implicit class EvenOps(private val even: Even) extends AnyVal {
def value: Int = unwrap(even)
def (other: Even): Even = wrap(even.value other.value)
def -(other: Even): Even = wrap(even.value - other.value)
}
}
You are creating type Even
which compiler knows nothing about, so it cannot prove that an arbitrary value is its instance. But you can force-cast to it an back again - if JVM in runtime won't be able to catch some issue with it, there is not problem (and since it assumes nothing about Even
it cannot disprove anything by contradiction).
Since Even
resolves to Even.Type
- that is type Type
within Even
object - Scala's implicit scope will automatically fetch all implicits that are defined in object Even
, so you can place your extension methods and typeclasses there.
This will help you pretend that this type has some methods defined.
In Scala 3 you can achieve the same with opaque type
. However this representation, has the nice side that it is easy to make it cross compilable with Scala 2 and Scala 3. As a matter of the fast, that's what Monix Newtype did, so you can use it instead of implementing this functionality yourself.
import monix.newtypes._
type Even = Even.Type
object Even extends Newtype[Int] {
// ...
}
Another option is older macro-annotation based library Scala Newtype. It will take your type defined as case class
and rewrite the code to implement something similar to what we have above:
import io.estatico.newtype.macros.newtype
@newtype case class Even(value: Int)
however it is harder to add your own smart constructor there, which is why it usually is paired with Refined Types. Then your code would look like:
import eu.timepit.refined._
import eu.timepit.refined.api.Refined
import eu.timepit.refined.numeric
import io.estatico.newtype.macros.newtype
@newtype case class Even(value: Int Refined numeric.Even)
object Even {
def parse(i: Int): Either[String, Even] =
refineV[numeric.Even](i).map(Even(_))
}
However, you might want to just use the plain refined type at this point, since Even
newtype wouldn't introduce any domain knowledge beyond what refinement does.