简体   繁体   English

运行时值的精简和存在类型

[英]Refined and existential types for runtime values

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. 假设我想在一些字符串和整数标识符之间进行映射,并且我希望我的类型使得无法获得运行时失败,因为有人试图查找超出范围的id。 Here's one straightforward API: 这是一个简单的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. 但是,如果用户通常会从getId获取他们的ID并因此知道它们是有效的,那么这很烦人。 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. 然后我们的用户可以使用Vocab[TagId]而不必担心在典型情况下检查getValue查找是否失败,但是如果需要,他们仍然可以查找任意整数。 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). 现在即使在编译时不知道S ,编译器仍然能够跟踪它给出的id在0和S之间的事实,这样我们就不必担心失败的可能性。我们回到值(如果我们当然使用相同的vocab实例)。

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 : 我可以通过为S提供Witness实例来编译它:

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. 我还可以将见证定义放在我的Vocab类中,然后导入vocab._以便在需要时使用它,但我真正想要的是能够提供refineV支持而无需额外的导入或定义。

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: 但是这仍然需要对每个vocab实例进行明确定义:

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). 我知道我可以用宏来实现witVocabS ,但我觉得应该有一个更好的方法来做这种事情,因为它看起来像一个非常合理的用例(我不是很熟悉精致,所以它完全是可能我错过了一些明显的东西)。

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 : 事实证明,这种工作方式,如果我们做的类型参数你想S被分配给它的单式混凝土values.size使用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 . 如果Scala允许单独类型的AnyVal ,我们可以删除sizeWitness并定义type S = sizeStable.type This limitation is lifted in the SIP-23 implementation . SIP-23实施中取消了这一限制。

Using refineV now just works even with the path dependant type vocab.P : 使用refineV现在只需使用路径依赖类型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: 这是有效的,因为编译器现在可以在Vocab实例的范围之外找到隐含的Witness.Aux[vocab.S]

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 . Validate[Int, vocab.P]现在使用这个隐式实例来构造一个Validate[Int, vocab.P]实例, refineV用它来决定Int是否是vocab有效索引。

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: 由于您用于优化Int的谓词依赖于Vocab ,因此一种解决方案是向Witness.Aux[S]添加隐式Witness.Aux[S]refineV的别名:

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: 现在使用Vocab.refine不需要任何额外的导入:

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM