2017-03-27 67 views
13

假設我想要在某些字符串和整數標識符之間進行映射,並且我希望我的類型能夠使運行時失敗,因爲有人試圖查找超出範圍的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不是在編譯時已知,則編譯器仍然能夠保持的事實IDS它給我們的軌道介於零和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] = [email protected] 

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] = [email protected] 

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

我知道我可以用宏實現witVocabS,但我覺得應該有更好的方法來做這種事情,因爲它看起來像是一個非常合理的用例(我對提煉不太熟悉,所以我完全有可能錯過某些明顯的東西)。

回答

11

事實證明,這種工作方式,如果我們通過賦予它牛逼使類型參數S具體你想使用shapeless.Witness他單類型的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) 
} 

如果斯卡拉將允許單種AnyVal S,我們可以刪除sizeWitness和定義type S = sizeStable.type。該限制在SIP-23 implementation中解除。

使用refineV現在只是甚至與路徑依賴型vocab.P工作:

scala> val vocab = new Vocab(Vector("foo", "bar", "baz")) 
vocab: Vocab = [email protected] 

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] = [email protected] 

scala> s.value 
res2: s.T = 3 

精現在使用此隱式實例構造一個Validate[Int, vocab.P]實例,其中refineV用於確定Int是否爲有效索引fo r vocab

+0

非常好,謝謝! –

+0

雖然我認爲這樣做可以取消刪除其他答案,因爲這是一種合理的替代方法。 –

3

由於您使用的煉製Int S中的謂語是依賴於Vocab,一種解決方案是爲refineV添加隱Witness.Aux[S]和別名到這個類:

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 = [email protected] 

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

謝謝! - 這絕對比我現在的選擇更好,但我仍然非常想讓'refineV'版本起作用,所以我現在要解決這個問題。 –