2017-03-27 3 views
13

Angenommen, ich möchte zwischen einigen Strings und Integer-Bezeichnern abbilden, und ich möchte, dass meine Typen es unmöglich machen, einen Laufzeitfehler zu erhalten, weil jemand versuchte, eine ID außerhalb des gültigen Bereichs zu suchen. Hier ist eine einfache API:Verfeinerte und existentielle Typen für Laufzeitwerte

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

Das ist ärgerlich, aber wenn die Benutzer in der Regel ihre IDs von getId bekommen und deshalb wissen, dass sie gültig sind. Im Folgenden ist eine Verbesserung in diesem Sinne:

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

Jetzt sind wir so etwas wie dieses haben könnte:

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 
} 

Und dann unsere Benutzer mit Vocab[TagId] arbeiten können und nicht befürchten zu prüfen, ob getValue Lookups fehlgeschlagen im typischen Fall, aber sie können immer noch beliebige ganze Zahlen nachschlagen, wenn sie müssen. Es ist immer noch ziemlich peinlich, da wir für jede Art von Ding, für das wir ein Vokabular haben wollen, einen eigenen Typ schreiben müssen.

Wir können auch mit refined etwas tun:

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

Nun, obwohl S nicht zum Zeitpunkt der Kompilierung bekannt ist, ist der Compiler noch in der Lage den Überblick über die Tatsache zu halten, dass die IDs es uns gibt sind zwischen Null und S, so dass wir uns keine Gedanken über die Möglichkeit eines Fehlers machen müssen, wenn wir zu den Werten zurückkehren (wenn wir dieselbe vocab Instanz natürlich verwenden).

Was ich will, ist in der Lage sein, dies zu schreiben:

val x = 2 
val vocab = new Vocab(Vector("foo", "bar", "qux")) 

eu.timepit.refined.refineV[vocab.P](x).map(vocab.getValue) 

Damit Benutzer auf einfache Weise beliebige ganze Zahlen sehen können, wenn sie wirklich brauchen. Dies lässt sich nicht kompilieren, aber:

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) 

Und natürlich scheitert es (zur Laufzeit aber sicher), wenn der Wert ist:

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

Ich kann durch die Bereitstellung eines Witness Instanz für S kompilieren machen außerhalb des Bereichs:

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

ich könnte auch die Zeugin Definition in meiner Vocab Klasse setzen und dann vocab._ importieren um es verfügbar zu machen, wenn ich das brauche, aber was ich wirklich will, ist in der Lage zu sein, refineV Unterstützung ohne zusätzliche Importe oder Definitionen zur Verfügung zu stellen.

Ich habe verschiedene Sachen wie diese versuche:

object Vocab { 
    implicit def witVocabS[V <: Vocab](implicit 
    witV: Witness.Aux[V] 
): Witness.Aux[V#S] = Witness.mkWitness(witV.value.size) 
} 

Dies erfordert aber auch eine explizite Definition für jedes vocab Beispiel:

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) 

Ich weiß, ich witVocabS mit einem Makro implementieren könnte, aber Ich habe das Gefühl, dass es einen schöneren Weg geben sollte, so etwas zu tun, da es ein ziemlich vernünftiger Anwendungsfall ist (und ich bin nicht sehr vertraut mit Verfeinerung, also ist es durchaus möglich, dass ich etwas Offensichtliches vermisse).

Antwort

11

stellt sich heraus, dass dies funktioniert, wie Sie möchten, wenn wir den Typ-Parameter S konkretisieren, indem sie die Zuordnung t er Singleton Art von values.size mit 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) 
} 

Wenn Scala Singleton Arten von AnyVal s erlauben würde, könnten wir sizeWitness entfernen und definieren type S = sizeStable.type. Diese Einschränkung wird in der SIP-23 implementation aufgehoben.

refineV Mit funktioniert jetzt nur noch mit dem Pfad abhängigen Typ 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) 

Dies funktioniert, da die Compiler nun eine implizites Witness.Aux[vocab.S] außerhalb des Anwendungsbereichs der Vocab Instanzen finden:

scala> val s = implicitly[shapeless.Witness.Aux[vocab.S]] 
s: shapeless.Witness.Aux[vocab.S] = [email protected] 

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

verfeinern verwendet nun diese implizite Instanz, um eine Validate[Int, vocab.P] Instanz zu konstruieren, die refineV verwendet, um zu entscheiden, ob ein Int gültiger Index fo ist r vocab.

+0

Sehr schön, danke! –

+0

Ich denke, es wäre sinnvoll, Ihre andere Antwort zu löschen, da es sich um einen vernünftigen alternativen Ansatz handelt. –

3

Da das Prädikat Sie zur Verfeinerung Int s verwenden ist abhängig von Vocab, ist eine Lösung, die ein implizites Witness.Aux[S] und eine Alias ​​für refineV zu dieser Klasse hinzuzufügen:

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

Mit Vocab.refine jetzt nicht benötigen keine zusätzliche Importe:

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

Danke! -dies ist definitiv besser als meine aktuellen Alternativen, aber ich würde immer noch gerne die 'fineV'-Version arbeiten lassen, also werde ich die Frage vorerst offen lassen. –

Verwandte Themen