Refined and existential types for runtime values
Asked Answered
R

2

16

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).

Reserve answered 27/3, 2017 at 15:55 Comment(0)
A
12

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 AnyVals, 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.

Atul answered 30/3, 2017 at 20:20 Comment(1)
I think it'd make sense to undelete your other answer, though, since it's a reasonable alternative approach.Reserve
A
4

Since the predicate you're using for refining Ints 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).)
Atul answered 30/3, 2017 at 5:47 Comment(1)
Thanks!—this is definitely better than my current alternatives, but I'd still really like to make the refineV version work, so I'm going to leave the question open for now.Reserve

© 2022 - 2024 — McMap. All rights reserved.