16

假设我想在一些字符串和整数标识符之间进行映射,并且我希望我的类型不会因为有人试图查找超出范围的 id 而导致运行时失败。这是一个简单的 API:

trait Vocab {
  def getId(value: String): Option[Int]
  def getValue(id: Int): Option[String] 
}

但是,如果用户通常会从中获取他们的 idgetId并因此知道它们是有效的,那么这很烦人。从这个意义上说,以下是一种改进:

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在典型情况下检查查找是否失败,但如果需要,他们仍然可以查找任意整数。但是,这仍然很尴尬,因为我们必须为每种我们想要词汇表的事物编写一个单独的类型。

我们也可以用提炼做这样的事情:

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在编译时不知道,编译器仍然能够跟踪它给我们的 ids 介于 0 和 之间的事实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)
                                          ^

我可以通过提供一个Witness实例来编译它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)

当然,当值超出范围时,它会失败(在运行时但安全):

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

我也可以将见证定义放在我的Vocab类中,然后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$1@1bde5374

scala> eu.timepit.refined.refineV[vocab.P](x).map(vocab.getValue)
res4: scala.util.Either[String,String] = Right(qux)

我知道我可以witVocabS用宏来实现,但我觉得应该有更好的方法来做这种事情,因为它看起来是一个非常合理的用例(而且我对提炼不是很熟悉,所以完全有可能我错过了一些明显的东西)。

4

2 回答 2

12

S事实证明,如果我们通过将类型参数指定为values.sizeusing的单例类型来具体化类型参数,这将如您所愿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)
}

如果 Scala 允许AnyVals 的单例类型,我们可以删除sizeWitness并定义s type S = sizeStable.type。这个限制在SIP-23 实现中被取消了。

即使使用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)

这是有效的,因为编译器现在可以找到实例Witness.Aux[vocab.S]范围之外的隐式:Vocab

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

精炼现在使用这个隐式实例来构造一个Validate[Int, vocab.P]实例,该实例refineV用于确定 anInt是否是 的有效索引vocab

于 2017-03-30T20:20:05.233 回答
4

由于您用于精炼Ints 的谓词依赖于,因此一种解决方案是为此类Vocab添加隐式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)
}

现在使用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).)
于 2017-03-30T05:47:45.510 回答