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).
Sehr schön, danke! –
Ich denke, es wäre sinnvoll, Ihre andere Antwort zu löschen, da es sich um einen vernünftigen alternativen Ansatz handelt. –