簡體   English   中英

運行時值的精簡和存在類型

[英]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查找是否失敗,但是如果需要,他們仍然可以查找任意整數。 但是,它仍然很尷尬,因為我們必須為每種我們想要詞匯表的東西編寫一個單獨的類型。

我們也可以用精致的方式做這樣的事情:

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在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)
                                          ^

我可以通過為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)

當然,當值超出范圍時,它會失敗(在運行時但安全):

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 ,但我覺得應該有一個更好的方法來做這種事情,因為它看起來像一個非常合理的用例(我不是很熟悉精致,所以它完全是可能我錯過了一些明顯的東西)。

事實證明,這種工作方式,如果我們做的類型參數你想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)
}

如果Scala允許單獨類型的AnyVal ,我們可以刪除sizeWitness並定義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)

這是有效的,因為編譯器現在可以在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

Validate[Int, vocab.P]現在使用這個隱式實例來構造一個Validate[Int, vocab.P]實例, refineV用它來決定Int是否是vocab有效索引。

由於您用於優化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)
}

現在使用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