Уточненные и экзистенциальные типы для значений времени выполнения
Предположим, я хочу отобразить между некоторыми строками и целочисленными идентификаторами, и я хочу, чтобы мои типы делали невозможным получение ошибки во время выполнения, потому что кто-то пытался найти идентификатор, который был вне диапазона. Вот один простой API:
trait Vocab {
def getId(value: String): Option[Int]
def getValue(id: Int): Option[String]
}
Это раздражает, однако, если пользователи обычно получают свои идентификаторы от getId
и поэтому знаю, что они действительны. Следующее улучшение в этом смысле:
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
не известно во время компиляции, компилятор все еще может отслеживать тот факт, что идентификаторы, которые он нам дает, находятся между нулем и 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
с макросом, но я чувствую, что должен быть более хороший способ сделать такие вещи, так как это кажется довольно разумным вариантом использования (и я не очень знаком с усовершенствованным, так что вполне возможно, что я скучаю что-то очевидное).
2 ответа
Оказывается, это работает так, как вы хотели бы, если бы мы сделали параметр типа 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)
Это работает, так как теперь компилятор может найти неявный 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
использует, чтобы решить, если Int
действительный индекс для vocab
,
Поскольку предикат, который вы используете для уточнения Int
s зависит от 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).)