运行时值的精炼类型和存在类型
Refined and existential types for runtime values
假设我想在一些字符串和整数标识符之间进行映射,并且我希望我的类型不会因为有人试图查找超出范围的 ID 而导致运行时失败。这是一个简单的 API:
trait Vocab {
def getId(value: String): Option[Int]
def getValue(id: Int): Option[String]
}
不过,如果用户通常会从 getId
获取他们的 ID 并因此知道它们是有效的,那么这很烦人。以下是这种意义上的改进:
trait Vocab[Id] {
def getId(value: String): Option[Id]
def getValue(id: Id): String
}
现在我们可以有这样的东西:
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
}
然后我们的用户可以使用 Vocab[TagId]
而不必担心在典型情况下检查 getValue
查找是否失败,但如果需要,他们仍然可以查找任意整数。不过,这仍然很尴尬,因为我们必须为每种需要词汇表的事物编写一个单独的类型。
我们也可以用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)
}
现在即使 S
在编译时未知,编译器仍然能够跟踪它给我们的 ID 在零和 S
之间的事实,因此当我们返回值时,我们不必担心失败的可能性(当然,如果我们使用相同的 vocab
实例)。
我想要的是能够这样写:
val x = 2
val vocab = new Vocab(Vector("foo", "bar", "qux"))
eu.timepit.refined.refineV[vocab.P](x).map(vocab.getValue)
以便用户在真正需要时可以轻松查找任意整数。但是,这不会编译:
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)
^
我可以通过为 S
:
提供一个 Witness
实例来编译它
scala> implicit val witVocabS: Witness.Aux[vocab.S] = Witness.mkWitness(vocab.size)
witVocabS: shapeless.Witness.Aux[vocab.S] = shapeless.Witness$$anon@485aac3c
scala> eu.timepit.refined.refineV[vocab.P](x).map(vocab.getValue)
res1: scala.util.Either[String,String] = Right(qux)
当然,当值超出范围时它会失败(在运行时但安全地):
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).)
我也可以将 witness 定义放在我的 Vocab
class 中,然后导入 vocab._
以便在我需要时可以使用它,但我真正想要的是能够无需额外导入或定义即可提供 refineV
支持。
我试过很多这样的东西:
object Vocab {
implicit def witVocabS[V <: Vocab](implicit
witV: Witness.Aux[V]
): Witness.Aux[V#S] = Witness.mkWitness(witV.value.size)
}
但这仍然需要对每个 vocab
实例进行显式定义:
scala> implicit val witVocabS: Witness.Aux[vocab.S] = Vocab.witVocabS
witVocabS: shapeless.Witness.Aux[vocab.S] = shapeless.Witness$$anon@1bde5374
scala> eu.timepit.refined.refineV[vocab.P](x).map(vocab.getValue)
res4: scala.util.Either[String,String] = Right(qux)
我知道我可以用宏实现 witVocabS
,但我觉得应该有更好的方法来做这种事情,因为它看起来是一个非常合理的用例(而且我对 refined 不是很熟悉,所以我完全有可能遗漏了一些明显的东西。
由于您用于优化 Int
s 的谓词依赖于 Vocab
,一种解决方案是为 refineV
添加隐式 Witness.Aux[S]
和别名为此 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)
}
现在使用 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).)
事实证明,如果我们通过使用 shapeless.Witness
:
将类型参数 S
指定为 values.size
的单例类型来使类型参数具体化,那么这将如您所愿地工作
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)
}
如果 Scala 允许 AnyVal
的单例类型,我们可以删除 sizeWitness
并定义 type S = sizeStable.type
。此限制在 SIP-23 implementation 中解除。
使用 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)
这是可行的,因为编译器现在可以找到 Vocab
实例范围之外的隐式 Witness.Aux[vocab.S]
:
scala> val s = implicitly[shapeless.Witness.Aux[vocab.S]]
s: shapeless.Witness.Aux[vocab.S] = shapeless.Witness$$anon@16cd7aa2
scala> s.value
res2: s.T = 3
refined 现在使用这个隐式实例来构造一个 Validate[Int, vocab.P]
实例,refineV
使用它来决定 Int
是否是 vocab
.[=28= 的有效索引]
假设我想在一些字符串和整数标识符之间进行映射,并且我希望我的类型不会因为有人试图查找超出范围的 ID 而导致运行时失败。这是一个简单的 API:
trait Vocab {
def getId(value: String): Option[Int]
def getValue(id: Int): Option[String]
}
不过,如果用户通常会从 getId
获取他们的 ID 并因此知道它们是有效的,那么这很烦人。以下是这种意义上的改进:
trait Vocab[Id] {
def getId(value: String): Option[Id]
def getValue(id: Id): String
}
现在我们可以有这样的东西:
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
}
然后我们的用户可以使用 Vocab[TagId]
而不必担心在典型情况下检查 getValue
查找是否失败,但如果需要,他们仍然可以查找任意整数。不过,这仍然很尴尬,因为我们必须为每种需要词汇表的事物编写一个单独的类型。
我们也可以用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)
}
现在即使 S
在编译时未知,编译器仍然能够跟踪它给我们的 ID 在零和 S
之间的事实,因此当我们返回值时,我们不必担心失败的可能性(当然,如果我们使用相同的 vocab
实例)。
我想要的是能够这样写:
val x = 2
val vocab = new Vocab(Vector("foo", "bar", "qux"))
eu.timepit.refined.refineV[vocab.P](x).map(vocab.getValue)
以便用户在真正需要时可以轻松查找任意整数。但是,这不会编译:
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)
^
我可以通过为 S
:
Witness
实例来编译它
scala> implicit val witVocabS: Witness.Aux[vocab.S] = Witness.mkWitness(vocab.size)
witVocabS: shapeless.Witness.Aux[vocab.S] = shapeless.Witness$$anon@485aac3c
scala> eu.timepit.refined.refineV[vocab.P](x).map(vocab.getValue)
res1: scala.util.Either[String,String] = Right(qux)
当然,当值超出范围时它会失败(在运行时但安全地):
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).)
我也可以将 witness 定义放在我的 Vocab
class 中,然后导入 vocab._
以便在我需要时可以使用它,但我真正想要的是能够无需额外导入或定义即可提供 refineV
支持。
我试过很多这样的东西:
object Vocab {
implicit def witVocabS[V <: Vocab](implicit
witV: Witness.Aux[V]
): Witness.Aux[V#S] = Witness.mkWitness(witV.value.size)
}
但这仍然需要对每个 vocab
实例进行显式定义:
scala> implicit val witVocabS: Witness.Aux[vocab.S] = Vocab.witVocabS
witVocabS: shapeless.Witness.Aux[vocab.S] = shapeless.Witness$$anon@1bde5374
scala> eu.timepit.refined.refineV[vocab.P](x).map(vocab.getValue)
res4: scala.util.Either[String,String] = Right(qux)
我知道我可以用宏实现 witVocabS
,但我觉得应该有更好的方法来做这种事情,因为它看起来是一个非常合理的用例(而且我对 refined 不是很熟悉,所以我完全有可能遗漏了一些明显的东西。
由于您用于优化 Int
s 的谓词依赖于 Vocab
,一种解决方案是为 refineV
添加隐式 Witness.Aux[S]
和别名为此 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)
}
现在使用 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).)
事实证明,如果我们通过使用 shapeless.Witness
:
S
指定为 values.size
的单例类型来使类型参数具体化,那么这将如您所愿地工作
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)
}
如果 Scala 允许 AnyVal
的单例类型,我们可以删除 sizeWitness
并定义 type S = sizeStable.type
。此限制在 SIP-23 implementation 中解除。
使用 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)
这是可行的,因为编译器现在可以找到 Vocab
实例范围之外的隐式 Witness.Aux[vocab.S]
:
scala> val s = implicitly[shapeless.Witness.Aux[vocab.S]]
s: shapeless.Witness.Aux[vocab.S] = shapeless.Witness$$anon@16cd7aa2
scala> s.value
res2: s.T = 3
refined 现在使用这个隐式实例来构造一个 Validate[Int, vocab.P]
实例,refineV
使用它来决定 Int
是否是 vocab
.[=28= 的有效索引]