[英]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.