Reputation: 139048
Suppose I want to map between some strings and integer identifiers, and I want my types to make it impossible to get a runtime failure because someone tried to look up an id that was out of range. Here's one straightforward API:
trait Vocab {
def getId(value: String): Option[Int]
def getValue(id: Int): Option[String]
}
This is annoying, though, if users will typically be getting their ids from getId
and therefore know they're valid. The following is an improvement in that sense:
trait Vocab[Id] {
def getId(value: String): Option[Id]
def getValue(id: Id): String
}
Now we could have something like this:
class TagId private(val value: Int) extends AnyVal
object TagId {
val tagCount: Int = 100
def fromInt(id: Int): Option[TagId] =
if (id >= 0 && id < tagCount) Some(new TagId(id)) else None
}
And then our users can work with Vocab[TagId]
and not have to worry about checking whether getValue
lookups failed in the typical case, but they can still look up arbitrary integers if they need to. It's still pretty awkward, though, since we have to write a separate type for each kind of thing we want a vocabulary for.
We can also do something like this with refined:
import eu.timepit.refined.api.Refined
import eu.timepit.refined.numeric.Interval.ClosedOpen
import shapeless.Witness
class Vocab(values: Vector[String]) {
type S <: Int
type P = ClosedOpen[Witness.`0`.T, S]
def size: S = values.size.asInstanceOf[S]
def getId(value: String): Option[Refined[Int, P]] = values.indexOf(value) match {
case -1 => None
case i => Some(Refined.unsafeApply[Int, P](i))
}
def getValue(id: Refined[Int, P]): String = values(id.value)
}
Now even though S
isn't known at compile time, the compiler is still able to keep track of the fact that the ids it gives us are between zero and S
, so that we don't have to worry about the possibility of failure when we go back to values (if we're using the same vocab
instance, of course).
What I want is to be able to write this:
val x = 2
val vocab = new Vocab(Vector("foo", "bar", "qux"))
eu.timepit.refined.refineV[vocab.P](x).map(vocab.getValue)
So that users can easily look up arbitrary integers when they really need to. This doesn't compile, though:
scala> eu.timepit.refined.refineV[vocab.P](x).map(vocab.getValue)
<console>:17: error: could not find implicit value for parameter v: eu.timepit.refined.api.Validate[Int,vocab.P]
eu.timepit.refined.refineV[vocab.P](x).map(vocab.getValue)
^
I can make it compile by providing a Witness
instance for S
:
scala> implicit val witVocabS: Witness.Aux[vocab.S] = Witness.mkWitness(vocab.size)
witVocabS: shapeless.Witness.Aux[vocab.S] = shapeless.Witness$$anon$1@485aac3c
scala> eu.timepit.refined.refineV[vocab.P](x).map(vocab.getValue)
res1: scala.util.Either[String,String] = Right(qux)
And of course it fails (at runtime but safely) when the value is out of range:
scala> val y = 3
y: Int = 3
scala> println(eu.timepit.refined.refineV[vocab.P](y).map(vocab.getValue))
Left(Right predicate of (!(3 < 0) && (3 < 3)) failed: Predicate failed: (3 < 3).)
I could also put the witness definition inside my Vocab
class and then import vocab._
to make it available when I need this, but what I really want is to be able to provide refineV
support without extra imports or definitions.
I've tried various stuff like this:
object Vocab {
implicit def witVocabS[V <: Vocab](implicit
witV: Witness.Aux[V]
): Witness.Aux[V#S] = Witness.mkWitness(witV.value.size)
}
But this still requires an explicit definition for each vocab
instance:
scala> implicit val witVocabS: Witness.Aux[vocab.S] = Vocab.witVocabS
witVocabS: shapeless.Witness.Aux[vocab.S] = shapeless.Witness$$anon$1@1bde5374
scala> eu.timepit.refined.refineV[vocab.P](x).map(vocab.getValue)
res4: scala.util.Either[String,String] = Right(qux)
I know I could implement witVocabS
with a macro, but I feel like there should be a nicer way to do this kind of thing, since it seems like a pretty reasonable use case (and I'm not very familiar with refined, so it's entirely possible that I'm missing something obvious).
Upvotes: 16
Views: 2181
Reputation: 4785
Turns out that this works as you would like if we make the type parameter S
concrete by assigning it the singleton type of values.size
using shapeless.Witness
:
import eu.timepit.refined.api.Refined
import eu.timepit.refined.numeric.Interval.ClosedOpen
import shapeless.Witness
class Vocab(values: Vector[String]) {
val sizeStable: Int = values.size
val sizeWitness = Witness(sizeStable)
type S = sizeWitness.T
type P = ClosedOpen[Witness.`0`.T, S]
def size: S = sizeWitness.value
def getId(value: String): Option[Refined[Int, P]] = values.indexOf(value) match {
case -1 => None
case i => Some(Refined.unsafeApply[Int, P](i))
}
def getValue(id: Refined[Int, P]): String = values(id.value)
}
If Scala would allow singleton types of AnyVal
s, we could remove sizeWitness
and define type S = sizeStable.type
. This limitation is lifted in the SIP-23 implementation.
Using refineV
now just works even with the path dependant type vocab.P
:
scala> val vocab = new Vocab(Vector("foo", "bar", "baz"))
vocab: Vocab = Vocab@5fae6bb9
scala> refineV[vocab.P](2)
res0: Either[String,eu.timepit.refined.api.Refined[Int,vocab.P]] = Right(2)
scala> refineV[vocab.P](4)
res1: Either[String,eu.timepit.refined.api.Refined[Int,vocab.P]] = Left(Right predicate of (!(4 < 0) && (4 < 3)) failed: Predicate failed: (4 < 3).)
scala> refineV[vocab.P](2).map(vocab.getValue)
res2: scala.util.Either[String,String] = Right(baz)
This works since the compiler can now find an implicit Witness.Aux[vocab.S]
outside the scope of Vocab
instances:
scala> val s = implicitly[shapeless.Witness.Aux[vocab.S]]
s: shapeless.Witness.Aux[vocab.S] = shapeless.Witness$$anon$1@16cd7aa2
scala> s.value
res2: s.T = 3
refined now uses this implicit instance to construct a Validate[Int, vocab.P]
instance which refineV
uses to decide if an Int
is valid index for vocab
.
Upvotes: 12
Reputation: 4785
Since the predicate you're using for refining Int
s is dependant on Vocab
, one solution is to add an implicit Witness.Aux[S]
and an alias for refineV
to this class:
import eu.timepit.refined._
import eu.timepit.refined.api.Refined
import eu.timepit.refined.numeric.Interval.ClosedOpen
import shapeless.Witness
class Vocab(values: Vector[String]) {
type S <: Int
type P = ClosedOpen[Witness.`0`.T, S]
def size: S = values.size.asInstanceOf[S]
def getId(value: String): Option[Refined[Int, P]] = values.indexOf(value) match {
case -1 => None
case i => Some(Refined.unsafeApply[Int, P](i))
}
def getValue(id: Refined[Int, P]): String = values(id.value)
implicit val witnessS: Witness.Aux[S] = Witness.mkWitness(size)
def refine(i: Int): Either[String, Refined[Int, P]] =
refineV[P](i)
}
Using Vocab.refine
now doesn't need any additional imports:
scala> val vocab = new Vocab(Vector("foo", "bar", "baz"))
vocab: Vocab = Vocab@490b83b3
scala> vocab.refine(1)
res4: Either[String,eu.timepit.refined.api.Refined[Int,vocab.P]] = Right(1)
scala> vocab.refine(3)
res5: Either[String,eu.timepit.refined.api.Refined[Int,vocab.P]] = Left(Right predicate of (!(3 < 0) && (3 < 3)) failed: Predicate failed: (3 < 3).)
Upvotes: 4