Уточненные и экзистенциальные типы для значений времени выполнения
Предположим, что я хочу сопоставить некоторые строки и целые идентификаторы, и я хочу, чтобы мои типы делали невозможным получение сбоя во время выполнения, потому что кто-то пытался найти идентификатор, который был вне диапазона. Вот простой 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
в типичном случае, но они все равно могут искать произвольные целые числа, если это необходимо. Это все еще довольно неудобно, поскольку мы должны написать отдельный тип для каждого типа вещей, для которого мы хотим использовать словарь.
Мы также можем сделать что-то подобное с помощью 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
не известно во время компиляции, компилятор все еще может отслеживать тот факт, что идентификаторы, которые он дает нам, находятся между нулем и 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] = [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
с помощью макроса, но я чувствую, что должен быть более удобный способ делать такие вещи, поскольку он кажется довольно разумным вариантом использования (и я не очень хорошо знаком с утонченный, так что вполне возможно, что мне не хватает чего-то очевидного).
Ответы
Ответ 1
Оказывается, что это работает так, как вам хотелось бы, если бы мы сделали конкретный тип 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
s, мы можем удалить sizeWitness
и определить type S = sizeStable.type
. Это ограничение снимается в SIP-23.
Использование 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)
Это работает, поскольку компилятор теперь может найти неявный Witness.Aux[vocab.S]
вне области экземпляров Vocab
:
scala> val s = implicitly[shapeless.Witness.Aux[vocab.S]]
s: shapeless.Witness.Aux[vocab.S] = [email protected]
scala> s.value
res2: s.T = 3
refined теперь использует этот неявный экземпляр для создания экземпляра Validate[Int, vocab.P]
, который refineV
использует, чтобы решить, является ли Int
допустимым индексом для Vocab
.
Ответ 2
Поскольку предикат, который вы используете для уточнения Int
, зависит от 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 = [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).)